從零開始:輕鬆掌握 JavaScript 的迭代器

更新日期: 2024 年 3 月 4 日

迭代器是一種「設計模式」,允許開發者以一種統一的方式遍歷集合中的每一項數據。

什麼是「設計模式」?

想像你在準備一頓大餐,有許多不同的菜需要烹飪,每道菜都有其獨特的烹飪步驟和技巧。

如果你每次烹飪都要重新發明烹飪方法,那將是非常耗時和低效的。

這時,你可以依賴一本好的食譜書,其中包含了各種烹飪的標準作法,如何準備原料、調味、控制火候等,這些食譜就像是「設計模式」,提供了解決烹飪問題的標準方法。

  • 創建型模式:針對食譜中的基礎原料準備部分,告訴你如何高效地準備和組合原料。比如,如何將一塊肉處理成適合烹飪的大小和形狀。
  • 結構型模式:相當於食譜中的烹飪組合指導,指導你如何將不同的食材組合到一起,以製作出美味的菜肴。例如,如何將炸好的雞塊配上特製的醬汁,使其風味獨特。
  • 行為型模式:則類似於食譜中的烹飪技巧和步驟,指導你在烹飪過程中應該如何操作。

其中,迭代器就屬於「行為型模式」,可以想像成是一本食譜中的索引或目錄,它允許你順序地查找和準備每道菜,而不需要事先知道整本食譜的所有內容。

這樣,無論食譜中有多少道菜,你都可以通過索引一步步地找到,並專注於當前準備的那一道,逐一完成整個大餐的準備。

在 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 的值是利用「閉包機制」,讓電腦「記住」了它上一次被調用結束後的數值。

閉包的工作原理:

  1. 函數嵌套函數:在 JavaScript 中,你可以在一個函數內部定義另一個函數。
  2. 內部函數訪問外部函數的變數:內部函數可以訪問定義它的外部函數中的變數。
  3. 外部函數返回內部函數:當外部函數執行後返回其內部函數,即使外部函數的執行上下文已經結束,返回的函數仍然可以訪問外部函數中的變數。
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 編碼中的進階應用。

Similar Posts