Javascript Iterable object(可迭代對(duì)象)

2023-02-17 10:49 更新

可迭代(Iterable) 對(duì)象是數(shù)組的泛化。這個(gè)概念是說(shuō)任何對(duì)象都可以被定制為可在 ?for..of? 循環(huán)中使用的對(duì)象。

數(shù)組是可迭代的。但不僅僅是數(shù)組。很多其他內(nèi)建對(duì)象也都是可迭代的。例如字符串也是可迭代的。

如果從技術(shù)上講,對(duì)象不是數(shù)組,而是表示某物的集合(列表,集合),for..of 是一個(gè)能夠遍歷它的很好的語(yǔ)法,因此,讓我們來(lái)看看如何使其發(fā)揮作用。

Symbol.iterator

通過(guò)自己創(chuàng)建一個(gè)對(duì)象,我們就可以輕松地掌握可迭代的概念。

例如,我們有一個(gè)對(duì)象,它并不是數(shù)組,但是看上去很適合使用 for..of 循環(huán)。

比如一個(gè) range 對(duì)象,它代表了一個(gè)數(shù)字區(qū)間:

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

// 我們希望 for..of 這樣運(yùn)行:
// for(let num of range) ... num=1,2,3,4,5

為了讓 range 對(duì)象可迭代(也就讓 for..of 可以運(yùn)行)我們需要為對(duì)象添加一個(gè)名為 Symbol.iterator 的方法(一個(gè)專(zhuān)門(mén)用于使對(duì)象可迭代的內(nèi)建 symbol)。

  1. 當(dāng) ?for..of? 循環(huán)啟動(dòng)時(shí),它會(huì)調(diào)用這個(gè)方法(如果沒(méi)找到,就會(huì)報(bào)錯(cuò))。這個(gè)方法必須返回一個(gè) 迭代器(iterator) —— 一個(gè)有 ?next? 方法的對(duì)象。
  2. 從此開(kāi)始,?for..of? 僅適用于這個(gè)被返回的對(duì)象。
  3. 當(dāng) ?for..of? 循環(huán)希望取得下一個(gè)數(shù)值,它就調(diào)用這個(gè)對(duì)象的 ?next()? 方法。
  4. ?next()? 方法返回的結(jié)果的格式必須是 ?{done: Boolean, value: any}?,當(dāng) ?done=true? 時(shí),表示循環(huán)結(jié)束,否則 ?value? 是下一個(gè)值。

這是帶有注釋的 range 的完整實(shí)現(xiàn):

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

// 1. for..of 調(diào)用首先會(huì)調(diào)用這個(gè):
range[Symbol.iterator] = function() {

  // ……它返回迭代器對(duì)象(iterator object):
  // 2. 接下來(lái),for..of 僅與下面的迭代器對(duì)象一起工作,要求它提供下一個(gè)值
  return {
    current: this.from,
    last: this.to,

    // 3. next() 在 for..of 的每一輪循環(huán)迭代中被調(diào)用
    next() {
      // 4. 它將會(huì)返回 {done:.., value :...} 格式的對(duì)象
      if (this.current <= this.last) {
        return { done: false, value: this.current++ };
      } else {
        return { done: true };
      }
    }
  };
};

// 現(xiàn)在它可以運(yùn)行了!
for (let num of range) {
  alert(num); // 1, 然后是 2, 3, 4, 5
}

請(qǐng)注意可迭代對(duì)象的核心功能:關(guān)注點(diǎn)分離。

  • ?range? 自身沒(méi)有 ?next()? 方法。
  • 相反,是通過(guò)調(diào)用 ?range[Symbol.iterator]()? 創(chuàng)建了另一個(gè)對(duì)象,即所謂的“迭代器”對(duì)象,并且它的 ?next? 會(huì)為迭代生成值。

因此,迭代器對(duì)象和與其進(jìn)行迭代的對(duì)象是分開(kāi)的。

從技術(shù)上說(shuō),我們可以將它們合并,并使用 ?range? 自身作為迭代器來(lái)簡(jiǎn)化代碼。

就像這樣:

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, 然后是 2, 3, 4, 5
}

現(xiàn)在 range[Symbol.iterator]() 返回的是 range 對(duì)象自身:它包括了必需的 next() 方法,并通過(guò) this.current 記憶了當(dāng)前的迭代進(jìn)程。這樣更短,對(duì)嗎?是的。有時(shí)這樣也可以。

但缺點(diǎn)是,現(xiàn)在不可能同時(shí)在對(duì)象上運(yùn)行兩個(gè) for..of 循環(huán)了:它們將共享迭代狀態(tài),因?yàn)橹挥幸粋€(gè)迭代器,即對(duì)象本身。但是兩個(gè)并行的 for..of 是很罕見(jiàn)的,即使在異步情況下。

無(wú)窮迭代器(iterator)

無(wú)窮迭代器也是可能的。例如,將 range 設(shè)置為 range.to = Infinity,這時(shí) range 則成為了無(wú)窮迭代器。或者我們可以創(chuàng)建一個(gè)可迭代對(duì)象,它生成一個(gè)無(wú)窮偽隨機(jī)數(shù)序列。也是可能的。

next 沒(méi)有什么限制,它可以返回越來(lái)越多的值,這是正常的。

當(dāng)然,迭代這種對(duì)象的 for..of 循環(huán)將不會(huì)停止。但是我們可以通過(guò)使用 break 來(lái)停止它。

字符串是可迭代的

數(shù)組和字符串是使用最廣泛的內(nèi)建可迭代對(duì)象。

對(duì)于一個(gè)字符串,for..of 遍歷它的每個(gè)字符:

for (let char of "test") {
  // 觸發(fā) 4 次,每個(gè)字符一次
  alert( char ); // t, then e, then s, then t
}

對(duì)于代理對(duì)(surrogate pairs),它也能正常工作!(譯注:這里的代理對(duì)也就指的是 UTF-16 的擴(kuò)展字符)

let str = '';
for (let char of str) {
    alert( char ); // ,然后是 
}

顯式調(diào)用迭代器

為了更深層地了解底層知識(shí),讓我們來(lái)看看如何顯式地使用迭代器。

我們將會(huì)采用與 ?for..of? 完全相同的方式遍歷字符串,但使用的是直接調(diào)用。這段代碼創(chuàng)建了一個(gè)字符串迭代器,并“手動(dòng)”從中獲取值。

let str = "Hello";

// 和 for..of 做相同的事
// 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); // 一個(gè)接一個(gè)地輸出字符
}

很少需要我們這樣做,但是比 for..of 給了我們更多的控制權(quán)。例如,我們可以拆分迭代過(guò)程:迭代一部分,然后停止,做一些其他處理,然后再恢復(fù)迭代。

可迭代(iterable)和類(lèi)數(shù)組(array-like)

這兩個(gè)官方術(shù)語(yǔ)看起來(lái)差不多,但其實(shí)大不相同。請(qǐng)確保你能夠充分理解它們的含義,以免造成混淆。

  • Iterable 如上所述,是實(shí)現(xiàn)了 ?Symbol.iterator? 方法的對(duì)象。
  • Array-like 是有索引和 ?length? 屬性的對(duì)象,所以它們看起來(lái)很像數(shù)組。

當(dāng)我們將 JavaScript 用于編寫(xiě)在瀏覽器或任何其他環(huán)境中的實(shí)際任務(wù)時(shí),我們可能會(huì)遇到可迭代對(duì)象或類(lèi)數(shù)組對(duì)象,或兩者兼有。

例如,字符串即是可迭代的(for..of 對(duì)它們有效),又是類(lèi)數(shù)組的(它們有數(shù)值索引和 length 屬性)。

但是一個(gè)可迭代對(duì)象也許不是類(lèi)數(shù)組對(duì)象。反之亦然,類(lèi)數(shù)組對(duì)象可能不可迭代。

例如,上面例子中的 range 是可迭代的,但并非類(lèi)數(shù)組對(duì)象,因?yàn)樗鼪](méi)有索引屬性,也沒(méi)有 length 屬性。

下面這個(gè)對(duì)象則是類(lèi)數(shù)組的,但是不可迭代:

let arrayLike = { // 有索引和 length 屬性 => 類(lèi)數(shù)組對(duì)象
  0: "Hello",
  1: "World",
  length: 2
};

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

可迭代對(duì)象和類(lèi)數(shù)組對(duì)象通常都 不是數(shù)組,它們沒(méi)有 push 和 pop 等方法。如果我們有一個(gè)這樣的對(duì)象,并想像數(shù)組那樣操作它,那就非常不方便。例如,我們想使用數(shù)組方法操作 range,應(yīng)該如何實(shí)現(xiàn)呢?

Array.from

有一個(gè)全局方法 Array.from 可以接受一個(gè)可迭代或類(lèi)數(shù)組的值,并從中獲取一個(gè)“真正的”數(shù)組。然后我們就可以對(duì)其調(diào)用數(shù)組方法了。

例如:

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

let arr = Array.from(arrayLike); // (*)
alert(arr.pop()); // World(pop 方法有效)

在 (*) 行的 Array.from 方法接受對(duì)象,檢查它是一個(gè)可迭代對(duì)象或類(lèi)數(shù)組對(duì)象,然后創(chuàng)建一個(gè)新數(shù)組,并將該對(duì)象的所有元素復(fù)制到這個(gè)新數(shù)組。

如果是可迭代對(duì)象,也是同樣:

// 假設(shè) range 來(lái)自上文的例子中
let arr = Array.from(range);
alert(arr); // 1,2,3,4,5 (數(shù)組的 toString 轉(zhuǎn)化方法生效)

Array.from 的完整語(yǔ)法允許我們提供一個(gè)可選的“映射(mapping)”函數(shù):

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

可選的第二個(gè)參數(shù) mapFn 可以是一個(gè)函數(shù),該函數(shù)會(huì)在對(duì)象中的元素被添加到數(shù)組前,被應(yīng)用于每個(gè)元素,此外 thisArg 允許我們?yōu)樵摵瘮?shù)設(shè)置 this。

例如:

// 假設(shè) range 來(lái)自上文例子中

// 求每個(gè)數(shù)的平方
let arr = Array.from(range, num => num * num);

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

現(xiàn)在我們用 Array.from 將一個(gè)字符串轉(zhuǎn)換為單個(gè)字符的數(shù)組:

let str = '';

// 將 str 拆分為字符數(shù)組
let chars = Array.from(str);

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

與 str.split 方法不同,它依賴于字符串的可迭代特性。因此,就像 for..of 一樣,可以正確地處理代理對(duì)(surrogate pair)。(譯注:代理對(duì)也就是 UTF-16 擴(kuò)展字符。)

技術(shù)上來(lái)講,它和下面這段代碼做的是相同的事:

let str = '';

let chars = []; // Array.from 內(nèi)部執(zhí)行相同的循環(huán)
for (let char of str) {
  chars.push(char);
}

alert(chars);

……但 Array.from 精簡(jiǎn)很多。

我們甚至可以基于 Array.from 創(chuàng)建代理感知(surrogate-aware)的slice 方法(譯注:也就是能夠處理 UTF-16 擴(kuò)展字符的 slice 方法):

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

let str = '';

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

// 原生方法不支持識(shí)別代理對(duì)(譯注:UTF-16 擴(kuò)展字符)
alert( str.slice(1, 3) ); // 亂碼(兩個(gè)不同 UTF-16 擴(kuò)展字符碎片拼接的結(jié)果)

總結(jié)

可以應(yīng)用 for..of 的對(duì)象被稱(chēng)為 可迭代的。

  • 技術(shù)上來(lái)說(shuō),可迭代對(duì)象必須實(shí)現(xiàn) Symbol.iterator 方法。
    • ?obj[Symbol.iterator]()? 的結(jié)果被稱(chēng)為 迭代器(iterator)。由它處理進(jìn)一步的迭代過(guò)程。
    • 一個(gè)迭代器必須有 ?next()? 方法,它返回一個(gè) ?{done: Boolean, value: any}? 對(duì)象,這里 ?done:true? 表明迭代結(jié)束,否則 ?value? 就是下一個(gè)值。
  • ?Symbol.iterator? 方法會(huì)被 ?for..of? 自動(dòng)調(diào)用,但我們也可以直接調(diào)用它。
  • 內(nèi)建的可迭代對(duì)象例如字符串和數(shù)組,都實(shí)現(xiàn)了 ?Symbol.iterator?。
  • 字符串迭代器能夠識(shí)別代理對(duì)(surrogate pair)。(譯注:代理對(duì)也就是 UTF-16 擴(kuò)展字符。)

有索引屬性和 length 屬性的對(duì)象被稱(chēng)為 類(lèi)數(shù)組對(duì)象。這種對(duì)象可能還具有其他屬性和方法,但是沒(méi)有數(shù)組的內(nèi)建方法。

如果我們仔細(xì)研究一下規(guī)范 —— 就會(huì)發(fā)現(xiàn)大多數(shù)內(nèi)建方法都假設(shè)它們需要處理的是可迭代對(duì)象或者類(lèi)數(shù)組對(duì)象,而不是“真正的”數(shù)組,因?yàn)檫@樣抽象度更高。

Array.from(obj[, mapFn, thisArg]) 將可迭代對(duì)象或類(lèi)數(shù)組對(duì)象 obj 轉(zhuǎn)化為真正的數(shù)組 Array,然后我們就可以對(duì)它應(yīng)用數(shù)組的方法??蛇x參數(shù) mapFn 和 thisArg 允許我們將函數(shù)應(yīng)用到每個(gè)元素。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)