2022 年 7 月 15 日

可迭代物件

可迭代物件是陣列的一種概括。這個概念讓我們可以使用 for..of 迴圈來使用任何物件。

當然,陣列是可迭代的。但還有許多其他內建物件也是可迭代的。例如,字串也是可迭代的。

如果一個物件在技術上不是陣列,但代表某個集合(清單、集合),那麼 for..of 是用來迴圈它的絕佳語法,所以讓我們看看如何讓它運作。

Symbol.iterator

我們可以透過製作自己的可迭代物件來輕鬆掌握可迭代物件的概念。

例如,我們有一個物件不是陣列,但看起來適合用於 for..of

例如表示數字區間的 range 物件

let range = {
  from: 1,
  to: 5
};

// We want the for..of to work:
// for(let num of range) ... num=1,2,3,4,5

為了讓 range 物件可迭代(並讓 for..of 運作),我們需要在物件中新增一個名為 Symbol.iterator 的方法(一個專門用於此目的的內建符號)。

  1. for..of 開始時,它會呼叫該方法一次(如果找不到,就會出錯)。該方法必須回傳一個迭代器,也就是一個具有 next 方法的物件。
  2. 從此以後,for..of 只會使用回傳的物件
  3. for..of 需要下一個值時,它會在該物件上呼叫 next()
  4. next() 的結果必須具有 {done: Boolean, value: any} 的形式,其中 done=true 表示迴圈已完成,否則 value 為下一個值。

以下是 range 的完整實作,並附有註解

let range = {
  from: 1,
  to: 5
};

// 1. call to for..of initially calls this
range[Symbol.iterator] = function() {

  // ...it returns the iterator object:
  // 2. Onward, for..of works only with the iterator object below, asking it for next values
  return {
    current: this.from,
    last: this.to,

    // 3. next() is called on each iteration by the for..of loop
    next() {
      // 4. it should return the value as an object {done:.., value :...}
      if (this.current <= this.last) {
        return { done: false, value: this.current++ };
      } else {
        return { done: true };
      }
    }
  };
};

// now it works!
for (let num of range) {
  alert(num); // 1, then 2, 3, 4, 5
}

請注意可迭代物件的核心特點:分工。

  • range 本身沒有 next() 方法。
  • 相反地,另一個物件,也就是所謂的「迭代器」,會透過呼叫 range[Symbol.iterator]() 來建立,而它的 next() 會產生迭代的值。

因此,迭代器物件與它迭代的物件是分開的。

技術上來說,我們可以將它們合併,並使用 range 本身作為迭代器,以簡化程式碼。

像這樣

let range = {
  from: 1,
  to: 5,

  [Symbol.iterator]() {
    this.current = this.from;
    return this;
  },

  next() {
    if (this.current <= this.to) {
      return { done: false, value: this.current++ };
    } else {
      return { done: true };
    }
  }
};

for (let num of range) {
  alert(num); // 1, then 2, 3, 4, 5
}

現在 range[Symbol.iterator]() 會回傳 range 物件本身:它具有必要的 next() 方法,並在 this.current 中記住目前的迭代進度。較短?是的。而且有時這樣也很好。

缺點是現在不可能同時執行兩個 for..of 迴圈在物件上執行:它們會共用迭代狀態,因為只有一個迭代器,也就是物件本身。但即使在非同步場景中,兩個並行的 for-ofs 也是罕見的。

無限迭代器

無限迭代器也是可能的。例如,range 會在 range.to = Infinity 時變成無限。或者我們可以建立一個可迭代物件,以產生一個無限的偽亂數序列。這也很有用。

next 沒有限制,它可以回傳越來越多的值,這是正常的。

當然,對這種可迭代物件的 for..of 迴圈將會是無窮無盡的。但我們隨時可以使用 break 來停止它。

字串是可迭代的

陣列和字串是最廣泛使用的內建可迭代物件。

對於字串,for..of 會迴圈處理它的字元

for (let char of "test") {
  // triggers 4 times: once for each character
  alert( char ); // t, then e, then s, then t
}

而且它可以正確處理代理對!

let str = '𝒳😂';
for (let char of str) {
    alert( char ); // 𝒳, and then 😂
}

明確呼叫迭代器

為了更深入了解,讓我們看看如何明確使用迭代器。

我們將以與 for..of 完全相同的方式遍歷字串,但使用直接呼叫。此程式碼建立一個字串迭代器,並「手動」從中取得值

let str = "Hello";

// does the same as
// for (let char of str) alert(char);

let iterator = str[Symbol.iterator]();

while (true) {
  let result = iterator.next();
  if (result.done) break;
  alert(result.value); // outputs characters one by one
}

這很少需要,但它比 for..of 給予我們更多控制權。例如,我們可以分割迭代程序:遍歷一段時間,然後停止,執行其他操作,然後稍後繼續。

可迭代物件和類陣列

兩個官方術語看起來很相似,但有很大的不同。請務必充分了解它們,以避免混淆。

  • 可迭代物件是實作 Symbol.iterator 方法的物件,如上所述。
  • 類陣列是具有索引和 length 的物件,因此它們看起來像陣列。

當我們在瀏覽器或任何其他環境中將 JavaScript 用於實際任務時,我們可能會遇到可迭代物件或類陣列,或兩者兼具的物件。

例如,字串既是可迭代物件(for..of 對它們有效),也是類陣列(它們具有數字索引和 length)。

但可迭代物件可能不是類陣列。反之亦然,類陣列可能不是可迭代物件。

例如,上述範例中的 range 是可迭代物件,但不是類陣列,因為它沒有索引屬性和 length

以下是類陣列但不可迭代的物件

let arrayLike = { // has indexes and length => array-like
  0: "Hello",
  1: "World",
  length: 2
};

// Error (no Symbol.iterator)
for (let item of arrayLike) {}

可迭代物件和類陣列通常不是陣列,它們沒有 pushpop 等。如果我們有這樣的物件並希望像使用陣列一樣使用它,那會相當不方便。例如,我們希望使用陣列方法來處理 range。如何實現?

Array.from

有一個通用方法 Array.from,它會取得可迭代物件或類陣列值,並從中建立一個「真正的」Array。然後我們可以在它上面呼叫陣列方法。

例如

let arrayLike = {
  0: "Hello",
  1: "World",
  length: 2
};

let arr = Array.from(arrayLike); // (*)
alert(arr.pop()); // World (method works)

(*) 行的 Array.from 會取得物件,檢查它是否為可迭代物件或類陣列,然後建立一個新陣列並將所有項目複製到其中。

可迭代物件也是如此

// assuming that range is taken from the example above
let arr = Array.from(range);
alert(arr); // 1,2,3,4,5 (array toString conversion works)

Array.from 的完整語法也允許我們提供一個可選的「對應」函式

Array.from(obj[, mapFn, thisArg])

可選的第二個參數 mapFn 可以是一個函式,它會在將每個元素加入陣列之前套用,而 thisArg 允許我們為它設定 this

例如

// assuming that range is taken from the example above

// square each number
let arr = Array.from(range, num => num * num);

alert(arr); // 1,4,9,16,25

這裡我們使用 Array.from 將一個字串轉換成一個字元陣列

let str = '𝒳😂';

// splits str into array of characters
let chars = Array.from(str);

alert(chars[0]); // 𝒳
alert(chars[1]); // 😂
alert(chars.length); // 2

str.split 不同,它依賴於字串的可迭代性質,因此,就像 for..of 一樣,可以正確處理代理對。

技術上來說,這裡它與下列作法相同

let str = '𝒳😂';

let chars = []; // Array.from internally does the same loop
for (let char of str) {
  chars.push(char);
}

alert(chars);

…但它比較簡潔。

我們甚至可以在它上面建立一個支援代理對的 slice

function slice(str, start, end) {
  return Array.from(str).slice(start, end).join('');
}

let str = '𝒳😂𩷶';

alert( slice(str, 1, 3) ); // 😂𩷶

// the native method does not support surrogate pairs
alert( str.slice(1, 3) ); // garbage (two pieces from different surrogate pairs)

摘要

可以在 for..of 中使用的物件稱為可迭代物件

  • 技術上來說,可迭代物件必須實作名為 Symbol.iterator 的方法。
    • obj[Symbol.iterator]() 的結果稱為迭代器。它處理進一步的迭代程序。
    • 迭代器必須有一個名為 next() 的方法,它會傳回一個物件 {done: 布林值, value: 任意值},這裡 done:true 表示迭代程序結束,否則 value 就是下一個值。
  • Symbol.iterator 方法會由 for..of 自動呼叫,但我們也可以直接呼叫它。
  • 內建的可迭代物件,例如字串或陣列,也實作了 Symbol.iterator
  • 字串迭代器知道代理對。

具有索引屬性和 length 的物件稱為類陣列物件。此類物件也可能具有其他屬性和方法,但缺少陣列的內建方法。

如果我們查看規格,我們會看到大多數內建方法假設它們處理的是可迭代物件或類陣列物件,而不是「真正的」陣列,因為那更抽象。

Array.from(obj[, mapFn, thisArg]) 從一個可迭代物件或類陣列物件 obj 建立一個真正的 Array,然後我們可以在它上面使用陣列方法。可選參數 mapFnthisArg 允許我們對每個項目套用一個函式。

教學地圖

評論

在評論之前先閱讀這段文字…
  • 如果你有改善建議,請 提交 GitHub 問題 或發起拉取請求,而不是留言。
  • 如果你無法理解文章中的某個部分,請詳細說明。
  • 要插入幾行程式碼,請使用 <code> 標籤,要插入多行程式碼,請將它們包在 <pre> 標籤中,要插入 10 行以上的程式碼,請使用沙盒 (plnkrjsbincodepen…)