for of 的原理解析

前言

for...of 是ES6引入用來遍歷所有數據結構的統一方法。

這裡的所有數據結構只指具有 iterator接口 的數據。一個數據只要部署了 Symbol.iterator ,就具有了 iterator 接口,就可以使用 for...of 循環遍歷它的成員。也就是說,for...of循環內部調用的數據結構為 Symbol.iterator 方法。

for...of 循環可以使用的範圍包括數組、Set 和 Map 結構、某些類似數組的對象(比如 arguments 對象、 DOM NodeList 對象)、 Generator 對象,以及字符串。也就是說上面提到的這些數據類型原生就具備了 iterator 接口。

所以千萬不要錯誤地認為 for...of 只是用來遍歷數組的。

Iterator

為什麼引入 Iterator

為什麼會有 會引入 Iterator 呢,是因為 ES6添加了 Map , Set ,再加上原有的數組,對象,一共就是4種表示 “集合”的數據結構。沒有 Map 和 Set 之前,我們都知道 for...in 一般是常用來遍歷對象, for 循環

常用來遍歷數據,現在引入的 Map , Set ,難道還要單獨為他們引入適合用來遍歷各自的方法麼。聰明的你肯定能想到,我們能不能提供一個方法來遍歷所有的數據結構呢,這個方法能遍歷所有的數據結構,一定是這些數據結構要有一些通用的一些特徵,然後這個公共的方法會根據這些通用的特徵去進行遍歷。

Iterator 就可以理解為是上面我們所說的通用的特徵。

我們來看看官方對 Iterator 是怎麼解釋的:遍歷器( Iterator )就是這樣一種機制。它是一種接口,為各種不同的數據結構提供統一的訪問機制。任何數據結構只要部署 Iterator 接口,就可以完成遍歷操作(即依次處理該數據結構的所有成員)。通俗點理解就是為了解決不同數據結構遍歷的問題,引入了 Iterator .

Iterator 是什麼樣子的呢

我們來模擬實現以下:

<code>function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{
value: array[nextIndex++],
done: false
}
:
{
value: undefined,
done: true
};
}
};
}
const it = makeIterator(['a', 'b']);

it.next()
// { value: "a", done: false }
it.next()
// { value: "b", done: false }
it.next()
// { value: undefined, done: true }/<code>

簡單解釋一下上面 array[nextIndex++] 是什麼意思,

假如 nextIndex 當前為0,則 nextIndex++ 的意思為1.返回0 2. 值自增( nextIndex 現在為1)。之前遇到一道面試題就是考察 i++ 和 ++i

<code>let number = 0
console.log(number++)
console.log(++number)
console.log(number)/<code>

輸出什麼?

答案是: 0, 2, 2;

一元后自增運算符 ++:

  1. 返回值(返回 0)
  2. 值自增(number 現在是 1)

一元前自增運算符 ++:

  1. 自增(number 現在是 2)
  2. 返回值(返回 2)

結果是 0 2 2.

好了,接著來看 Iterator 的整個的遍歷過程:

  1. 創建一個指針對象(上面代碼中的 it ),指向當前數據的起始位置
  2. 第一次調用指針對象的 next 方法,可以將指針指向數據結構的第一個成員(上面代碼中的 a )。
  3. 第二次調用指針對象的 next 方法,可以將指針指向數據結構的第二個成員(上面代碼中的 b )。
  4. 不斷調用指針對象的 next 方法,直到它指向數據結構的結束位置

每一次調用next方法,都會返回數據結構的當前成員的信息。具體來說,就是返回一個包含 value 和 done 兩個屬性的對象。其中, value 屬性是當前成員的值, done 屬性是一個布爾值,表示遍歷是否結束,即是否要有必要再一次調用。

Iterator 的特點

  • 各種數據結構,提供一個統一的、簡便的訪問接口
  • 使得數據結構的成員能夠按某種次序排列
  • ES6 創造了一種新的遍歷命令 for...of 循環, Iterator 接口主要供 for...of 消費

默認 Iterator 接口

部署在 Symbol.iterator 屬性,或者說,一個數據結構只要具有 Symbol.iterator 屬性,就認為是"可遍歷的"。

原生具備 Iterator 接口的數據結構如下。

  • Array
  • Map
  • Set
  • String :字符串是一個類似數組的對象,也原生具有 Iterator 接口。
  • TypedArray :通俗理解:ArrayBuffer是一片內存空間,不能直接引用裡面的數據,可以通過TypedArray類型引用,用戶只能通過TypedArray使用這片內存,不能直接通過ArrayBuffer使用這片內存
  • 函數的 arguments 對象
  • NodeList 對象

除了原生具備 Iterator 接口的數據之外,其他數據結構(主要是對象)的 Iterator 接口,都需要自己在 Symbol.iterator 屬性上面部署,這樣才會被 for...of 循環遍歷。

對象( Object )之所以沒有默認部署 Iterator 接口,是因為對象的哪個屬性先遍歷,哪個屬性後遍歷是不確定的,需要開發者手動指定。本質上,遍歷器是一種線性處理,對於任何非線性的數據結構,部署遍歷器接口,就等於部署一種線性轉換。不過,嚴格地說,對象部署遍歷器接口並不是很必要,因為這時對象實際上被當作 Map 結構使用, ES5 沒有 Map 結構,而 ES6 原生提供了。

一個對象如果要具備可被 for...of 循環調用的 Iterator 接口,就必須在 Symbol.iterator 的屬性上部署遍歷器生成方法(原型鏈上的對象具有該方法也可)。

<code>class RangeIterator {
constructor(start, stop) {
this.value = start;
this.stop = stop;
}

[Symbol.iterator]() { return this; }

next() {
let value = this.value;
if (value < this.stop) {
this.value++;
return {
done: false,
value: value
};
}
return {
done: true,
value: undefined
};
}
}

function range(start, stop) {
return new RangeIterator(start, stop);
}

for (let value of range(0, 3)) {
console.log(value); // 0, 1, 2
}/<code>

如果 Symbol.iterator 方法對應的不是遍歷器生成函數(即會返回一個遍歷器對象),解釋引擎將會報錯。

<code>const obj = {};

obj[Symbol.iterator] = () => 1;

// TypeError: Result of the Symbol.iterator method is not an object
console.log([...obj] )/<code>

字符串是一個類似數組的對象,也原生具有 Iterator 接口。

<code>const someString = "hi";
typeof someString[Symbol.iterator]
// "function"/<code>

調用Iterator的場景

除了 for...of ,還有下面幾個場景

  • 解構賦值:對數組和 Set 結構進行解構賦值時,會默認調用Symbol.iterator方法。
  • 擴展運算符:擴展運算符內部就調用 Iterator 接口。
  • yield* : yield* 後面跟的是一個可遍歷的結構,它會調用該結構的遍歷器接口。
  • 接受數組作為參數的場合Array.from() Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]])) Promise.all() Promise.race()

Iterator的實現思想

看到 next 這個你有沒有感到很熟悉,鏈表中 每個元素由一個存儲元素本身的節點和一個指向下一個元素的引用(即next屬性)組成。是不是很類似,不錯, Iterator 的實現思想就是來源於單向鏈表。

下面來簡單介紹一下單向鏈表。

單向鏈表

鏈表存儲有序的元素集合,但不同於數組,鏈表中每個元素在內存中並不是連續放置的。每個元素由一個存儲元素本身的節點和一個指向下一個元素的節點(也稱為指針或鏈接)組成,下圖展示了一個鏈表的結構。

for of 的原理解析

和數組相比較,鏈表的一個好處已在於,添加或移除元素的時候不需要移動其他元素。然而,鏈表需要指針,因此實現鏈表時需要額外注意。數組的另一個細節是可以直接訪問任何位置的任何元素,而 想要訪問鏈表中間的一個元素,需要從起點(表頭)開始迭代列表知道找到所有元素 。

現實生活中也有一些鏈表的例子,比如說尋寶遊戲。你有一條線索,這條線索是指向尋找下一條線索的地點的指針。你順著這條鏈接去到下一個地點,得到另一條指向再下一處的線索,得到列表中間的線索的唯一辦法,就是從起點(第一條線索)順著列表尋找。

具體怎麼實現一個單向鏈表,這裡就不展開講了,推薦看 《學習JavaScript數據結構與算法》(第二版) 。

for...of 循環

關於 for...of 的原理,相信你看完上面的內容已經掌握的差不多了,現在我們以數組為例,說一下, for...of 和之前我們經常使用的其他循環方式有什麼不同。

最原始的寫法就是 for 循環。

<code>for (let i = 0; i < myArray.length; index++) { 

console.log(myArray[i]);
}/<code>

這種寫法比較麻煩,因此數組提供內置的 forEach 方法。

<code>myArray.forEach((value) => {
console.log(value);
});/<code>

這種寫法的問題在於,無法中途跳出 forEach 循環, break 命令或 return 命令都不能奏效。

for...in 循環可以遍歷數組的鍵名。

<code>const arr = ['red', 'green', 'blue'];
for(let v in arr) {
console.log(v); // '0', '1', '2
}/<code>

for...in 循環有幾個缺點:

<code>for...in
for...in
for...in
/<code>

for...in 循環主要是為遍歷對象而設計的,不適用於遍歷數組。

for...of 和上面幾種做法( for 循環, forEach , for...in )相比,有一些顯著的優點

  • 有著同 for...in 一樣的簡潔語法,但是沒有 for...in 那些缺點。
  • 不同於 forEach 方法,它可以與 break 、 continue 和 return 配合使用。
  • 提供了遍歷所有數據結構的統一操作接口。

總結

  • for...of 可以用來遍歷所有具有 iterator 接口的數據結構。(一個數據結構只要部署了 Symbol.iterator 屬性,就被視為具有 iterator 接口)。也就是說 for...of 循環內部調用是數據結構的 Symbol.iterator
  • iterator 的實現思想來源於 單向鏈表
  • forEach 循環中無法用 break 命令或 return 命令終止。而 for...of 可以。
  • for...in 遍歷數組遍歷的是鍵名,所有適合遍歷對象, for...of 遍歷數組遍歷的是鍵值。


分享到:


相關文章: