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種表示 「集合」的數據結構。沒有 MapSet以前,咱們都知道 for...in通常是經常使用來遍歷對象,for循環 經常使用來遍歷數據,如今引入的 Map, Set,難道還要單獨爲他們引入適合用來遍歷各自的方法麼。聰明的你確定能想到,咱們能不能提供一個方法來遍歷全部的數據結構呢,這個方法能遍歷全部的數據結構,必定是這些數據結構要有一些通用的一些特徵,而後這個公共的方法會根據這些通用的特徵去進行遍歷。微信

Iterator就能夠理解爲是上面咱們所說的通用的特徵。數據結構

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

Iterator 是什麼樣子的呢

咱們來模擬實現如下:學習

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 }
複製代碼

簡單解釋一下上面 array[nextIndex++]是什麼意思, 假如nextIndex當前爲0,則 nextIndex++的意思爲1.返回0 2. 值自增(nextIndex如今爲1)。以前遇到一道面試題就是考察 i++++iui

let number = 0
console.log(number++)
console.log(++number)
console.log(number)
複製代碼

輸出什麼?this

答案是: 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方法,都會返回數據結構的當前成員的信息。具體來講,就是返回一個包含valuedone兩個屬性的對象。其中,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的屬性上部署遍歷器生成方法(原型鏈上的對象具備該方法也可)。

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
}
複製代碼

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

const obj = {};

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

// TypeError: Result of the Symbol.iterator method is not an object
console.log([...obj] )
複製代碼

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

const someString = "hi";
typeof someString[Symbol.iterator]
// "function"
複製代碼

調用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的實現思想就是來源於單向鏈表。

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

單向鏈表

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

鏈表

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

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

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

for...of 循環

關於for...of的原理,相信你看完上面的內容已經掌握的差很少了,如今咱們以數組爲例,說一下,for...of 和以前咱們常用的其餘循環方式有什麼不一樣。

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

for (let i = 0; i < myArray.length; index++) {
  console.log(myArray[i]);
}
複製代碼

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

myArray.forEach((value) => {
  console.log(value);
});
複製代碼

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

for...in循環能夠遍歷數組的鍵名。

const arr = ['red', 'green', 'blue'];
for(let v in arr) {
    console.log(v); // '0', '1', '2
}
複製代碼

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

  • 數組的鍵名是數字,可是for...in循環是以字符串做爲鍵名「0」、「1」、「2」等等。
  • for...in循環不只遍歷數字鍵名,還會遍歷手動添加的其餘鍵,甚至包括原型鏈上的鍵
  • 某些狀況下,for...in循環會以任意順序遍歷鍵名。

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

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

  • 有着同for...in同樣的簡潔語法,可是沒有for...in那些缺點。
  • 不一樣於forEach方法,它能夠與breakcontinuereturn配合使用。
  • 提供了遍歷全部數據結構的統一操做接口。

總結

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

其餘

最近發起了一個100天前端進階計劃,主要是深挖每一個知識點背後的原理,歡迎關注 微信公衆號「牧碼的星星」,咱們一塊兒學習,打卡100天。

牧碼的星星
相關文章
相關標籤/搜索