Javascript Iterable object(可迭代對象)

2023-02-17 10:49 更新

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

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

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

Symbol.iterator

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

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

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

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

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

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

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

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

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

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

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

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

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

請注意可迭代對象的核心功能:關(guān)注點分離。

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

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

從技術(shù)上說,我們可以將它們合并,并使用 ?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, 然后是 2, 3, 4, 5
}

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

但缺點是,現(xiàn)在不可能同時在對象上運行兩個 for..of 循環(huán)了:它們將共享迭代狀態(tài),因為只有一個迭代器,即對象本身。但是兩個并行的 for..of 是很罕見的,即使在異步情況下。

無窮迭代器(iterator)

無窮迭代器也是可能的。例如,將 range 設(shè)置為 range.to = Infinity,這時 range 則成為了無窮迭代器?;蛘呶覀兛梢詣?chuàng)建一個可迭代對象,它生成一個無窮偽隨機數(shù)序列。也是可能的。

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

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

字符串是可迭代的

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

對于一個字符串,for..of 遍歷它的每個字符:

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

對于代理對(surrogate pairs),它也能正常工作?。ㄗg注:這里的代理對也就指的是 UTF-16 的擴展字符)

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

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

為了更深層地了解底層知識,讓我們來看看如何顯式地使用迭代器。

我們將會采用與 ?for..of? 完全相同的方式遍歷字符串,但使用的是直接調(diào)用。這段代碼創(chuà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); // 一個接一個地輸出字符
}

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

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

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

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

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

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

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

例如,上面例子中的 range 是可迭代的,但并非類數(shù)組對象,因為它沒有索引屬性,也沒有 length 屬性。

下面這個對象則是類數(shù)組的,但是不可迭代:

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

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

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

Array.from

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

例如:

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

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

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

如果是可迭代對象,也是同樣:

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

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

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

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

例如:

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

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

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

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

let str = '';

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

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

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

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

let str = '';

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

alert(chars);

……但 Array.from 精簡很多。

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

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

let str = '';

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

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

總結(jié)

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

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

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

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

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


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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號