從forEach到迭代器
本文從使用 forEach
對陣列進行遍歷開始說起,粗略對比使用 forEach
, for...in
, for...of
進行遍歷的差異,並由此引入 ES6 中 *可迭代物件/迭代器 *的概念,並對其進行粗略介紹。
forEach
forEach
方法按升序為陣列中的 有效值 的每一項執行一次 callback
函式
特點
- 只會對“有效值”進行操作,也就是說遍歷的時候會跳過 已刪除或者未初始化的項,即會跳過那些值為
empty
的內容。(undefined
不會被跳過哦)。

-
forEach
的第二個引數thisArg
傳入後可以改變callback
函式的this
值,如果不傳則為undefined
。當然,callback
所拿到的this
值也受普遍規律的支配:這意味著如果callback
是個箭頭函式,則thisArg
會被忽略。(因為箭頭函式已經在詞法上綁定了this值,不能再改了) - 不建議在迴圈中增/刪陣列內容:首先,
forEach
遍歷的範圍在第一次呼叫callback
前就會確定,這意味著呼叫forEach
後新增到陣列中的項不會被callback
訪問到。同時,由於forEach
的遍歷是基於下標的(可以這麼理解,並能從 Polyfill 中看到這一實現 ),那麼在迴圈中刪了陣列幾項內容則會有奇怪的事情發生:比如下圖中,在下標為 1 的時候執行了shift()
,那麼原來的第 3 項變為了第 2 項,原來的第 2 項則被跳過了。

缺點
- 無法中斷或跳出:如果想要中斷/跳出,可以考慮使用如下兩種方式:
- 使用
for
/for..of
- 使用
Array.prototype.every()
/Array.prototype.some()
並返回false
/true
來進行中斷/跳出
- 使用
- 無法鏈式呼叫(因為返回了
undefined
)
for...in
for...in
迴圈實際是為迴圈”enumerable“物件而設計的:
for...in
語句以任意順序遍歷一個物件的 可列舉屬性 。對於每個不同的屬性,語句都會被執行。
特點
- 遍歷可列舉屬性:
for...in
迴圈將遍歷物件本身的所有可列舉屬性,以及物件從其建構函式原型中繼承的屬性(更接近原型鏈中物件的屬性覆蓋原型屬性)。 - 可以被中斷
缺點
- 會訪問到能訪問到的所有的 __ 可列舉屬性__ ,也就是說會包括那些原型鏈上的屬性。如果想要僅迭代自身的屬性,那麼在使用
for...in
的同時還需要配合getOwnPropertyNames()
或hasOwnProperty()
- 不能保證
for ... in
將以任何特定的順序返回索引,比如在 IE 下可能會亂來。 - 不建議用於迭代
Array
:- 不一定保證按照下標順序
- 遍歷得到的下標是字串而不是數字
for...of
for...of
的本質是在 可迭代物件(iterable objects) 上呼叫其 迭代方法 建立一個迭代迴圈,並執行對應語句。可以迭代 陣列/字串/型別化陣列(TypedArray)/Map/Set/generators/類陣列物件(NodeList/arguments) 等。需要注意的是, Object
並不是一個可迭代物件。
for...of
是 ES6 中新出爐的,其彌補了 forEach
和 for...in
的諸多缺點:
- 可以使用
break
,throw
或return
等終止 - 能正常迭代陣列(依賴陣列預設的迭代行為)
區別
那麼我們簡單的幾句話說明一下 for...of
和 for...in
的區別:
- for...in 語句以原始插入順序(還不一定保證)迭代物件的 可列舉屬性 。
-
for...of
語句遍歷 可迭代物件 定義要迭代的資料。 -
for...of
得到的是 值(value), 而for...in
得到的是 鍵(key)。
那麼扯到了 可迭代物件 ,就不得不說說 ES6 中新增的與 可迭代物件/迭代器 有關東西了。
可迭代物件/迭代器
*iteration *是 ES6 中新引入的遍歷資料的機制,其核心概念是: iterable(可迭代物件) 和 iterator(迭代器):
- *iterable(可迭代物件):*一種希望被外界訪問的內部元素的資料結構,實現了
Symbol.iterator
方法 - *iterator(迭代器):*用於遍歷資料結構元素的指標
可迭代協議(iterable protocol)
首先,可迭代協議允許 JavaScript 物件去定義或定製它們的迭代行為。而如 Array
或 Map
等內建 可迭代物件有預設的迭代行為,而如 Object
則沒有。(所以為什麼不能對 Object
用 for...of
)再具體一點,即物件或其原型上有 [Symbol.iterator]
的方法,用於返回一個物件的無參函式,被返回物件符合迭代器協議。然後在該物件被迭代的時候,呼叫其 [Symbol.iterator]
方法獲得一個在迭代中使用的迭代器。
首先可以看見的是 Array 和 Map 在原型鏈上有該方法,而 Object 則沒有。這印證了上面對於哪些可以用於 for...of
的說法。

如果我們非要想用 for...of
對 Object
所擁有的屬性進行遍歷,則可使用內建的 Object.keys()
方法:
for (const key of Object.keys(someObject)) { console.log(key + ": " + someObject[key]); } 複製程式碼
或者如果你想要更簡單得同時得到鍵和值,可以考慮使用 Object.entries()
:
for (const [key, value] of Object.entries(someObject)) { console.log(key + ": " + value); } 複製程式碼
其次,有如下情況會使用可迭代物件的迭代行為:
-
for...of
- 擴充套件運算子(Spread syntax)
- yield*
- 解構賦值(Destructuring assignment )
- 一些可接受可迭代物件的內建API(本質是這些介面在接收一個數組作為引數的時候會呼叫陣列的迭代行為)
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()
- Promise.all() /Promise.race() 等
迭代器協議(iterator protocol)
上文說到返回了一個 迭代器 用於迭代,那我們就來看看符合什麼樣規範的才算一個 迭代器 。 只需要實現一個符合如下要求的 next
方法的物件即可:
屬性 |
值 |
next |
返回一個物件的無參函式,被返回物件擁有兩個屬性:
|
本質上,在使用一個迭代器的時候,會不斷呼叫其 next()
方法直到返回 done: true
。
自定義迭代行為
既然符合可迭代協議的均為可迭代物件,那接下來就簡單自定義一下迭代行為:
// 讓我們的陣列倒序輸出 value const myArr = [1, 2, 3]; myArr[Symbol.iterator] = function () { const that = this; let index = that.length; return { next: function () { if (index > 0) { index--; return { value: that[index], done: false }; } else { return { done: true }; } }, }; }; [...myArr]; // [3, 2, 1] Array.from(myArr) // [3, 2, 1] 複製程式碼
一句說明可迭代物件和迭代器的關係
當一個__可迭代物件__需要被迭代的時候,它的 Symbol.iterator
方法被無參呼叫, 然後返回一個用於在迭代中獲得值的 迭代器 。 換句話說,一個物件(或其原型)上有符合標準的 Symbol.iterator
介面,那他就是 可迭代的(Iterator)
,呼叫這個介面返回的物件就是一個 迭代器

關閉迭代器
上文提到說 for...of
比 forEach
好在其可以被“中斷”,那麼對於在 for...of
中中斷迭代,其本質是中斷了迭代器,迭代器在中斷後會被關閉。說到這裡,就繼續說一下迭代器關閉的情況了。
首先,迭代器的關閉分為兩種情況:
- Exhaustion:當被持續呼叫
next()
方法直到返回done: true
,也就是迭代器正常執行完後關閉 - Closing:通過呼叫
return()
方法來告訴迭代器不打算再呼叫next()
方法
那麼什麼時候會呼叫迭代器的 return
方法呢:
- 首先,
return()
是個可選方法,只有具有該方法的迭代器才是 可關閉的(closable) - 其次,只有當沒有 Exhaustion 時才應該呼叫
return()
,如break
,throw
或return
等
最後, return()
方法中也不是想怎麼寫就怎麼寫的,也有自己的要求, return()
方法需要符合以下規範:
-
return(x)
通常應該返回如{ done: true, value: x }
的結果,如果返回的不是個物件則會報錯 - 呼叫
return()
後,next()
返回的物件也應該是done:true
(這就是為什麼有一些迭代器在for...of
迴圈中中斷後無法再次使用的原因,比如Generator
)
同時,需要額外注意的是,及時在收到迭代器最後一個值後呼叫 break
等,也會觸發 return()
function createIterable() { let done = false; const iterable = { [Symbol.iterator]() { return this; }, next() { if (!done) { done = true; return { done: false, value: 'a' }; } else { return { done: true, value: undefined }; } }, return () { console.log('return() was called!'); return { done: true, value: undefined }; }, }; return iterable; } for (const value of createIterable()) { console.log(value); break; } 複製程式碼
生成器(Generator)
既是迭代器也是可迭代物件
上文 迭代器協議 中提到的返回的擁有 next()
方法的物件和我們在 Generator
中使用的 next()
方法似乎一模一樣。確實, Generator
符合可迭代協議和迭代器協議的。
因為 Generator
既有符合規範的 next()
(迭代器協議)方法,也有 Symbol.iterator
(可迭代協議)方法,因此它 既是迭代器也是可迭代物件 。
可關閉的( closable )
預設情況下,Generator物件是可關閉的。因此在用 for...of
時中斷迭代後,無法再次對原有 Generator物件進行迭代。(因為呼叫 return()
後, next()
返回的物件也應該是 done:true
)
當然,既然是預設情況,我們就可以想辦法讓其無法被關閉: 可以通過包裝一下迭代器,將迭代器本身/原型上的 return()
方法被重寫掉
class PreventReturn { constructor(iterator) { this.iterator = iterator; } [Symbol.iterator]() { return this; } next() { return this.iterator.next(); } // 重寫掉 return 方法 return (value = undefined) { return { done: false, value }; } } 複製程式碼
更多關於 Generator
的內容就不在本篇進行闡述,有機會將單獨作為一篇慢慢講。