從零開始:輕鬆掌握 JavaScript 的迭代器
更新日期: 2024 年 9 月 14 日
迭代器是一種「設計模式」,允許開發者以一種統一的方式遍歷集合中的每一項數據。
什麼是「設計模式」?
想像你在準備一頓大餐,有許多不同的菜需要烹飪,每道菜都有其獨特的烹飪步驟和技巧。
如果你每次烹飪都要重新發明烹飪方法,那將是非常耗時和低效的。
這時,你可以依賴一本好的食譜書,其中包含了各種烹飪的標準作法,如何準備原料、調味、控制火候等,這些食譜就像是「設計模式」,提供了解決烹飪問題的標準方法。
- 創建型模式:針對食譜中的基礎原料準備部分,告訴你如何高效地準備和組合原料。比如,如何將一塊肉處理成適合烹飪的大小和形狀。
- 結構型模式:相當於食譜中的烹飪組合指導,指導你如何將不同的食材組合到一起,以製作出美味的菜肴。例如,如何將炸好的雞塊配上特製的醬汁,使其風味獨特。
- 行為型模式:則類似於食譜中的烹飪技巧和步驟,指導你在烹飪過程中應該如何操作。
其中,迭代器就屬於「行為型模式」,可以想像成是一本食譜中的索引或目錄,它允許你順序地查找和準備每道菜,而不需要事先知道整本食譜的所有內容。
這樣,無論食譜中有多少道菜,你都可以通過索引一步步地找到,並專注於當前準備的那一道,逐一完成整個大餐的準備。
在 JavaScript 中,迭代器不僅僅是一種進階的數據處理工具,它還是理解和利用語言特性,如生成器(Generators)和異步編程的基礎。
無論你是剛開始學習 JavaScript,還是已經有一定開發經驗的程式設計師,掌握迭代器都將在你的開發生涯中起到關鍵作用。
本文將從最基本的概念開始,帶你輕鬆入門 JavaScript 的迭代器。
理解迭代器
在深入探索 JavaScript 的迭代器之前,讓我們先建立一個關於迭代器的基本理解。
一般來說,數據在不同的資料結構中,會以不同的方式去組織與管理。例如:
- 陣列(Array):數據以連續的方式存儲在記憶體中,可以通過索引直接訪問。
- 鏈表(LinkedList):每個元素都包含數據和指向下一個元素的指針,元素在記憶體中不一定連續存儲。
- 樹(Tree):數據以層次結構存儲,每個元素(節點)都有可能指向一個或多個子元素。
- 圖(Graph):由節點(數據元素)和邊(節點間的關聯)組成,節點間的關聯可以是複雜的。
為什麼使用迭代器
迭代器則為我們提供了一個統一的操作機制,通過這個它我們可以順序訪問集合中的每一個數據,而無需關心這些數據是如何在被組織或存儲的。
換句話說,當我們在使用迭代器時,可以專注於我們想要進行的操作(比如遍歷集合中的每個元素),而不用擔心集合內部是如何實現的。
這種抽象化讓我们能夠更簡單、更高效地撰寫程式碼,同時保持代馬的清晰與靈活性。
此外迭代器模式支援延遲加載(Lazy Loading),即數據只有在需要時才被處理,這可以提高代碼的效率和性能。
定義迭代器和遍歷
在 JavaScript 中,迭代器是一個具有 next() 方法的物件,該方法回傳一個包含兩個屬性:value 和 done 的物件。
value 屬性代表當前遍歷的值,而 done 是一個布爾值,指示遍歷是否完成。
迭代器如何工作
當迭代器的 next() 方法被調用時,它回傳集合中的下一個資料。
當結構中沒有更多資料時,done 屬性會變為 true,這代表迭代過程的結束。
這個過程使得迭代器可以與各種類型的數據結構一起工作,為數據處理提供了極大的靈活性和方便性。
迭代器的應用場景
- 陣列遍歷:使用迭代器可以輕鬆遍歷陣列元素,特別是在處理巨大陣列時,可以有效管理記憶體消耗。
- 自定義數據結構:對於自定義的數據結構,如樹或圖。藉由使用迭代器機制,可以使其容易被遍歷和使用。
- 異步編碼:迭代器模式也可以應用於異步編程中,使得數據的非同步加載更加方便。
JavaScript 中的迭代器應用
在 JavaScript 中,迭代器模式不僅是一種理論概念,而且是一種可實際應用的方法。
它允許開發者以一致的方式遍歷各種數據結構。
建立迭代器的基本步驟
要在 JavaScript 中創建一個迭代器,你需要定義一個返回 next 方法的對象。
這個 next 方法應當返回一個包含 value 和 done 兩個屬性的對象。
以下是一個簡單的迭代器示例:
// 定義了一個名為 createIterator 的函數,它接受一個參數 collection,這個參數預期是一個陣列或類似陣列的物件。
function createIterator(collection) {
// 在函數內部,定義了一個變數 index 並初始化為 0。這個變數用於追蹤當前遍歷到集合中的哪個位置。
let index = 0;
// 函數返回一個物件,這個物件包含了一個方法:next。
return {
next: function () {
// 在 next 方法內部,首先定義了一個常量 done,它是一個布林值,用於判斷遍歷是否已經完成。
// 如果 index 的值大於或等於 collection 的長度,說明所有的元素都已經遍歷完畢,此時 done 為 true。
const done = index >= collection.length;
// 定義另一個常量 value,它存儲當前遍歷到的元素的值。
// 如果 done 為 false(即還未完成遍歷),則從 collection 中取出 index 當前位置的元素,之後才會將 index 的值增加 1(index++)。
// 記住:index 是在取值後,才會再增加的。
// 如果 done 為 true,則說明沒有更多元素可遍歷,value 被設置為 undefined。
const value = !done ? collection[index++] : undefined;
// next 方法返回一個包含兩個屬性的物件:value 和 done。
// value 是當前遍歷到的元素的值,done 是一個布林值,表示遍歷是否已經完成。
return { value, done };
}
};
}
值得一提,上述代碼中 index 的值是利用「閉包機制」,讓電腦「記住」了它上一次被調用結束後的數值。
閉包的工作原理:
- 函數嵌套函數:在 JavaScript 中,你可以在一個函數內部定義另一個函數。
- 內部函數訪問外部函數的變數:內部函數可以訪問定義它的外部函數中的變數。
- 外部函數返回內部函數:當外部函數執行後返回其內部函數,即使外部函數的執行上下文已經結束,返回的函數仍然可以訪問外部函數中的變數。
const iterator = createIterator([1, 2, 3]);
console.log(iterator)
// 回傳:
// { next: [Function: next] }
console.log(iterator.next());
// 回傳:
// { value: 1, done: false }
console.log(iterator.next());
// 回傳:
// { value: 2, done: false }
console.log(iterator.next());
// 回傳:
// { value: 3, done: false }
console.log(iterator.next());
// 回傳:
// { value: undefined, done: true }
這段代碼展示了如何為一個陣列創建一個迭代器,並逐步遍歷這個陣列的每個元素。
使用 Symbol.iterator 定義自己的迭代器
Symbol 是 ES6 中新增的一種基本數據類型,它可以創建一個全局唯一的引用。
Symbol.iterator 是其中一個預定義好的屬性。
它本質上是一個在物件上定義的特殊屬性(鍵),該屬性對應的值應該是一個函數,這個函數返回一個遵循迭代器協議的物件。
const collection = {
items: [1, 2, 3],
[Symbol.iterator]: function() {
let index = 0;
return {
next: () => {
const done = index >= this.items.length;
const value = !done ? this.items[index++] : undefined;
return { value, done };
}
};
}
};
for (let item of collection) {
console.log(item); // 1, 2, 3
}
這段代碼通過 Symbol.iterator 方法使自定義物件 collection 成為可迭代的,從而可以使用 for…of 循環遍歷。
補充說明:for…of
for…of 循環是一種特殊的循環語法,它只對可迭代物件有效。
如果嘗試在不可迭代物件上使用 for…of 循環,JavaScript 會拋出一個錯誤,因為它找不到對象的 Symbol.iterator 方法。例如:
const obj = {a: 1, b: 2};
for (let x of obj) {
console.log(x);
}
// TypeError: obj is not iterable
在這個例子中,嘗試對一個普通物件 obj 使用 for…of 循環會導致錯誤,因為普通物件默認不是可迭代的,即沒有實現 Symbol.iterator 方法。
要讓一個普通物件像 {a: 1, b: 2} 變成可迭代的,你需要在這個物件上定義一個 Symbol.iterator 方法。
這個方法需要返回一個迭代器,這個迭代器必須有一個 next 方法,用於在每次迭代時返回對象的下一個元素:
const obj = {
a: 1,
b: 2,
[Symbol.iterator]: function() {
// 獲取對象的鍵(keys)並創建一個索引變量
const keys = Object.keys(this);
let index = 0;
// 返回一個迭代器對象
return {
// 實現 next 方法
next: () => {
if (index < keys.length) {
// 如果還沒有遍歷完對象的鍵,則返回下一個鍵的值
const key = keys[index++];
const value = this[key];
return { value: value, done: false };
} else {
// 如果已經遍歷完對象的鍵,則標記迭代結束
return { done: true };
}
}
};
}
};
// 使用 for...of 循環遍歷 obj 對象
for (let value of obj) {
console.log(value); // 依次輸出:1, 2
}
內建迭代器
JavaScript 中的許多內建類型,如陣列、字符串、Map 和 Set,都已經是可迭代物件。
因為它們的原型上都實現了迭代器方法。這意味著你可以直接在這些類型上使用 for…of 循環,而無需任何額外的設定或實現。
const numbers = [1, 2, 3];
for (let number of numbers) {
console.log(number); // 1, 2, 3
}
const string = "hello";
for (let character of string) {
console.log(character); // h e l l o
}
透過這些範例,我們可以看到,迭代器在 JavaScript 中的實現既直觀又強大。
它不僅為自定義數據結構提供了一種標準化的遍歷機制,也使得遍歷內建數據結構變得更加簡單和一致。
迭代器的進階使用
迭代器不僅僅是遍歷集合的工具,它們在現代 JavaScript 編程中扮演著更多的角色。讓我們來看看如何進階使用迭代器。
展開運算符(…)
展開運算符(…)是一個 ES6 引入的特性,它可以用來展開可迭代對象中的元素。
這在函數調用、陣列字面量中合併陣列,或者在物件字面量中合併物件時特別有用。
let numbers = [1, 2, 3];
let moreNumbers = [4, 5, ...numbers]; // [4, 5, 1, 2, 3]
console.log(moreNumbers);
這裡,展開運算符被用來合併兩個數組,它直接遍歷 numbers 數組,並將其元素插入到 moreNumbers 數組中。
解構賦值
解構賦值是 ES6 中引入的另一種新語法,它允許你直接從陣列或物件中提取值,並將它們賦值給變量,這樣做可以讓代碼更加簡潔易讀。
對於陣列
當應用於陣列時,你可以這樣使用解構賦值:
let [a, b] = [1, 2];
console.log(a); // 輸出 1
console.log(b); // 輸出 2
在這個例子中,[a, b] = [1, 2]; 將右邊陣列中的元素按順序賦值給左邊的 a 和 b。
這就是解構賦值的基本用法,它讓你不必逐一訪問陣列元素。
對於物件
解構賦值也可以應用於物件:
let {c, d} = {c: 3, d: 4};
console.log(c); // 輸出 3
console.log(d); // 輸出 4
在這個例子中,{c, d} 從物件中按鍵名提取對應的值,並賦值給同名變量 c 和 d。
對於展開運算符
解構賦值允許你將可迭代物件(如陣列或者具有迭代器的物件)解構到變量中。
這種語法不僅使代碼更加簡潔,而且也提高了代碼的可讀性。
let [first, second, ...rest] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(second); // 2
console.log(rest); // [3, 4, 5]
在這個例子中,我們使用解構賦值來提取數組中的前兩個元素,並使用剩餘運算符(…)來獲取數組中的其餘部分。
生成器與迭代器
生成器是 ES6 中引入的一種特殊類型的函數,它可以通過 yield 關鍵字暫停和恢復其執行。
生成器函數在語法上與普通函數不同,它們由 function* 關鍵字定義。
生成器的基本語法
生成器函數在每次遇到 yield 表達式時暫停,並返回一個遵循迭代器協議的物件。
該對象包含了 value 和 done 兩個屬性。當 next() 方法被調用時,生成器函數會從上次暫停的位置繼續執行,直到遇到下一個 yield,或函數結束。
function* simpleGenerator() {
yield 1;
yield 2;
yield 3;
}
const generatorObject = simpleGenerator();
console.log(generatorObject.next()); // { value: 1, done: false }
console.log(generatorObject.next()); // { value: 2, done: false }
console.log(generatorObject.next()); // { value: 3, done: false }
console.log(generatorObject.next()); // { value: undefined, done: true }
這段代碼展示了一個簡單的生成器函數,它逐步產生三個數字。
如何使用生成器創建迭代器
生成器提供了一種便捷的方式,來創建遵循迭代器協議的物件。
這使得它們非常適合於實現自定義的迭代邏輯。
function* range(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
const numbers = range(1, 5);
for (let number of numbers) {
console.log(number); // 1, 2, 3, 4, 5
}
這裡,range 生成器函數模擬了 Python 中的 range 函數,它能夠產生一個數字序列。
總結
在本文中,我們從基礎開始,深入探討了 JavaScript 中的迭代器和生成器,涵蓋了它們的定義、實現方式,以及在現代 JavaScript 編碼中的進階應用。