可叠代(Iterable) 對象是數組的泛化。這個概念是說任何對象都可以被定制爲可在 for..of
循環中使用的對象。
數組是可叠代的。但不僅僅是數組。很多其他內建對象也都是可叠代的。例如字符串也是可叠代的。
如果從嚴格意義上講,對象不是數組,而是表示某物的集合(列表,集合),for..of
是壹個能夠遍曆它的很好的語法,因此,讓我們來看看如何使其發揮作用。
通過自己創建壹個對象,我們就可以輕松地掌握可叠代的概念。
例如,我們有壹個對象,它並不是數組,但是看上去很適合使用 for..of
循環。
比如壹個 range
對象,它代表了壹個數字區間:
let range = { from: 1, to: 5 }; // 我們希望 for..of 這樣運行: // for(let num of range) ... num=1,2,3,4,5
爲了讓 range
對象可叠代(也就讓 for..of
可以運行)我們需要爲對象添加壹個名爲 Symbol.iterator
的方法(壹個專門用于使對象可叠代的內建 symbol)。
當 for..of
循環啓動時,它會調用這個方法(如果沒找到,就會報錯)。這個方法必須返回壹個 叠代器(iterator) —— 壹個有 next
方法的對象。
從此開始,for..of
僅適用于這個被返回的對象。
當 for..of
循環希望取得下壹個數值,它就調用這個對象的 next()
方法。
next()
方法返回的結果的格式必須是 {done: Boolean, value: any}
,當 done=true
時,表示循環結束,否則 value
是下壹個值。
這是帶有注釋的 range
的完整實現:
let range = { from: 1, to: 5 }; // 1. for..of 調用首先會調用這個: range[Symbol.iterator] = function() { // ……它返回叠代器對象(iterator object): // 2. 接下來,for..of 僅與下面的叠代器對象壹起工作,要求它提供下壹個值 return { current: this.from, last: this.to, // 3. next() 在 for..of 的每壹輪循環叠代中被調用 next() { // 4. 它將會返回 {done:.., value :...} 格式的對象 if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; }; // 現在它可以運行了! for (let num of range) { alert(num); // 1, 然後是 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, 然後是 2, 3, 4, 5 }
現在 range[Symbol.iterator]()
返回的是 range
對象自身:它包括了必需的 next()
方法,並通過 this.current
記憶了當前的叠代進程。這樣更短,對嗎?是的。有時這樣也可以。
但缺點是,現在不可能同時在對象上運行兩個 for..of
循環了:它們將共享叠代狀態,因爲只有壹個叠代器,即對象本身。但是兩個並行的 for..of
是很罕見的,即使在異步情況下。
無窮叠代器(iterator)
無窮叠代器也是可能的。例如,將 range
設置爲 range.to = Infinity
,這時 range
則成爲了無窮叠代器。或者我們可以創建壹個可叠代對象,它生成壹個無窮僞隨機數序列。也是可能的。
next
沒有什麽限制,它可以返回越來越多的值,這是正常的。
當然,叠代這種對象的 for..of
循環將不會停止。但是我們可以通過使用 break
來停止它。
數組和字符串是使用最廣泛的內建可叠代對象。
對于壹個字符串,for..of
遍曆它的每個字符:
for (let char of "test") { // 觸發 4 次,每個字符壹次 alert( char ); // t, then e, then s, then t }
對于代理對(surrogate pairs),它也能正常工作!(譯注:這裏的代理對也就指的是 UTF-16 的擴展字符)
let str = '??'; for (let char of str) { alert( char ); // ?,然後是 ? }
爲了更深層地了解底層知識,讓我們來看看如何顯式地使用叠代器。
我們將會采用與 for..of
完全相同的方式遍曆字符串,但使用的是直接調用。這段代碼創建了壹個字符串叠代器,並“手動”從中獲取值。
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
給了我們更多的控制權。例如,我們可以拆分叠代過程:叠代壹部分,然後停止,做壹些其他處理,然後再恢複叠代。
這兩個官方術語看起來差不多,但其實大不相同。請確保妳能夠充分理解它們的含義,以免造成混淆。
Iterable 如上所述,是實現了 Symbol.iterator
方法的對象。
Array-like 是有索引和 length
屬性的對象,所以它們看起來很像數組。
當我們將 JavaScript 用于編寫在浏覽器或任何其他環境中的實際任務時,我們可能會遇到可叠代對象或類數組對象,或兩者兼有。
例如,字符串即是可叠代的(for..of
對它們有效),又是類數組的(它們有數值索引和 length
屬性)。
但是壹個可叠代對象也許不是類數組對象。反之亦然,類數組對象可能不可叠代。
例如,上面例子中的 range
是可叠代的,但並非類數組對象,因爲它沒有索引屬性,也沒有 length
屬性。
下面這個對象則是類數組的,但是不可叠代:
let arrayLike = { // 有索引和 length 屬性 => 類數組對象 0: "Hello", 1: "World", length: 2 }; // Error (no Symbol.iterator) for (let item of arrayLike) {}
可叠代對象和類數組對象通常都 不是數組,它們沒有 push
和 pop
等方法。如果我們有壹個這樣的對象,並想像數組那樣操作它,那就非常不方便。例如,我們想使用數組方法操作 range
,應該如何實現呢?
有壹個全局方法 Array.from 可以接受壹個可叠代或類數組的值,並從中獲取壹個“真正的”數組。然後我們就可以對其調用數組方法了。
例如:
let arrayLike = { 0: "Hello", 1: "World", length: 2 }; let arr = Array.from(arrayLike); // (*) alert(arr.pop()); // World(pop 方法有效)
在 (*)
行的 Array.from
方法接受對象,檢查它是壹個可叠代對象或類數組對象,然後創建壹個新數組,並將該對象的所有元素複制到這個新數組。
如果是可叠代對象,也是同樣:
// 假設 range 來自上文的例子中 let arr = Array.from(range); alert(arr); // 1,2,3,4,5 (數組的 toString 轉化方法生效)
Array.from
的完整語法允許我們提供壹個可選的“映射(mapping)”函數:
Array.from(obj[, mapFn, thisArg])
可選的第二個參數 mapFn
可以是壹個函數,該函數會在對象中的元素被添加到數組前,被應用于每個元素,此外 thisArg
允許我們爲該函數設置 this
。
例如:
// 假設 range 來自上文例子中 // 求每個數的平方 let arr = Array.from(range, num => num * num); alert(arr); // 1,4,9,16,25
現在我們用 Array.from
將壹個字符串轉換爲單個字符的數組:
let str = '??'; // 將 str 拆分爲字符數組 let chars = Array.from(str); alert(chars[0]); // ? alert(chars[1]); // ? alert(chars.length); // 2
與 str.split
方法不同,它依賴于字符串的可叠代特性。因此,就像 for..of
壹樣,可以正確地處理代理對(surrogate pair)。(譯注:代理對也就是 UTF-16 擴展字符。)
技術上來講,它和下面這段代碼做的是相同的事:
let str = '??'; let chars = []; // Array.from 內部執行相同的循環 for (let char of str) { chars.push(char); } alert(chars);
……但 Array.from
精簡很多。
我們甚至可以基于 Array.from
創建代理感知(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 擴展字符碎片拼接的結果)
可以應用 for..of
的對象被稱爲 可叠代的。
技術上來說,可叠代對象必須實現 Symbol.iterator
方法。
obj[Symbol.iterator]()
的結果被稱爲 叠代器(iterator)。由它處理進壹步的叠代過程。
壹個叠代器必須有 next()
方法,它返回壹個 {done: Boolean, value: any}
對象,這裏 done:true
表明叠代結束,否則 value
就是下壹個值。
Symbol.iterator
方法會被 for..of
自動調用,但我們也可以直接調用它。
內建的可叠代對象例如字符串和數組,都實現了 Symbol.iterator
。
字符串叠代器能夠識別代理對(surrogate pair)。(譯注:代理對也就是 UTF-16 擴展字符。)
有索引屬性和 length
屬性的對象被稱爲 類數組對象。這種對象可能還具有其他屬性和方法,但是沒有數組的內建方法。
如果我們仔細研究壹下規範 —— 就會發現大多數內建方法都假設它們需要處理的是可叠代對象或者類數組對象,而不是“真正的”數組,因爲這樣抽象度更高。
Array.from(obj[, mapFn, thisArg])
將可叠代對象或類數組對象 obj
轉化爲真正的數組 Array
,然後我們就可以對它應用數組的方法。可選參數 mapFn
和 thisArg
允許我們將函數應用到每個元素。