JavaScript淺談之迭代器(Iterator) 和for-of迴圈

ding_發表於2018-09-13

起源

使用for迴圈遍歷陣列

我們如何遍歷陣列元素?在javascript剛出現的時候,我們可能會這樣進行陣列遍歷

for (var i = 0; index < Array.length; index++) {
  console.log(Array[i]);
}
複製程式碼

程式碼看起來很簡單對吧,但是仔細閱讀程式碼,我們僅僅只需要取陣列元素的值,卻需要提前獲取陣列長度和宣告索引變數等,當有多層迴圈巢狀的時候,就需要更多的索引變數,大大增加了程式碼的複雜度。於是乎,在ES5中就出現了for-Each方法來進行遍歷

使用forEach遍歷陣列

使用forEach遍歷陣列,可以這樣寫

Array.forEach(function (value) {
    console.log(value);
})
複製程式碼

這段程式碼看起來很簡單了,但是也有一些小問題,不能使用break語句中斷迴圈也不能使用return語句返回到外層函式,當然了,使用for迴圈去遍歷陣列也是個不錯的選擇, 不過,還可以用一種方式遍歷陣列,那就是for-in

for-in遍歷陣列

使用for-in遍歷陣列,程式碼如下

for (var i in Array) { // 千萬別這樣做
  console.log(Array[i]);
}
複製程式碼

但是這個選擇建議不要使用,為什麼呢?

  • 在這段程式碼中 i的值實際上不是下標的數字,而是字串"0","1","2","3",此時很可能在無意間進行字串運算,例如 “2” + 1 == “21” ,這樣會給編碼過程帶來很大的麻煩
  • for-in 不僅可以遍歷陣列元素,還可以遍歷自定義屬性,例如,如果你的陣列中有一個可列舉屬性Array.username,迴圈將額外進行遍歷一次,遍歷到索引為‘username'的索引
  • for-in還有可能會隨機順序遍歷陣列元素 總之,for-in是為普通物件量身定製的,你可以遍歷得到字串型別的鍵,因此不適用於陣列遍歷。 現在我們來講一個高大上的陣列遍歷方式-for-of迴圈

for-of迴圈

簡介

自從引入ES6以來,就增加了很多新的東西,而for-of迴圈就是其中一種,話不多說,直接上程式碼看看for-of迴圈長什麼樣,其實很簡單

for (var value of Array) {
  console.log(value);
}
複製程式碼

看到這行程式碼是不是覺得很眼熟,對的,長得很像for-in把,我們來探討一下for-of迴圈的外表下到底隱藏了多少強大的功能吧,首先需要記住以下幾點

  • for-of是最簡單,最直接的遍歷陣列元素的語法
  • 成功的避開了for-in迴圈的所有缺陷
  • 與forEach不同的是,它可以正確的相應break,continue和return語句

for-of可以用來幹什麼呢?

首先,他可以用來遍歷物件屬性,也可以用來遍歷資料(陣列中的值),不僅如此!for-of迴圈還可以遍歷其他的集合 for-of不僅支援陣列遍歷,還支援大多數類陣列物件,例如DOM NodeList for-of迴圈也支援字串遍歷, 同樣,也支援Map和Set物件遍歷。舉個簡單的例子

 // 基於單詞陣列建立一個set物件
var Words = new Set(words);
複製程式碼

生成了一個Set物件之後,就可以輕鬆地使用for-of遍歷它所包含的內容了。

for (var value of Words) {
   console.log(value);
}
複製程式碼

遍歷Map物件與遍歷Set物件稍有不同,那是因為Map比較特殊,它裡面的資料有鍵值對組成,所以需要用解構(destructuring)來將鍵值對拆解為兩個獨立的變數

for (var [key, value] of BookMap) {
   console.log(key + "'s phone number is: " + value);
}
複製程式碼

解構,他是ES6新增的特性下面對解構進行一個簡單的瞭解

解構

什麼是解構賦值? 解構賦值允許你使用類似陣列或物件字面量的語法將陣列和物件的屬性賦給各種變數。這種賦值語法極度簡潔,同時還比傳統的屬性訪問方法更為清晰。

通常來講,你很可能這樣訪問陣列中的前三個元素:

var first = array[0]
var second = array[1]
var third = array[2]
複製程式碼

如果使用解構賦值的特性,將會使等效的程式碼變得更加簡潔並且可讀性更高:

var [first, second, third] = array
複製程式碼

以上是陣列解構賦值的一個簡單示例,其語法的一般形式為:

[ variable1, variable2, ..., variableN ] = array;
複製程式碼

關於解構我就不一一講述了,現在我們回到主題,for-of迴圈的強大之處,未來的JS可以使用一些新型的集合類,甚至會有更多的型別陸續誕生,而for-of就是為遍歷所有這些集合特別設計的迴圈語句。 for-of迴圈不支援普通物件,但如果你想迭代一個物件的屬性,你可以用for-in迴圈(這也是它的本職工作)或者內建方Object.keys()

// 向控制檯輸出物件的可列舉屬性
for (var key of Object.keys(Object)) {
  console.log(key + ": " + Object[key]);
}
複製程式碼

內部原理

被新增到ES6的那些新特性並不是無章可循,裡面大多數特性其他語言都在使用,也證明了這些特性很有用,就拿 for-of 語句來說,在 C++、JAVA、C# 和 Python 中都存在類似的迴圈語句,並且用於遍歷這門語言和其標準庫中的各種資料結構。 與其他語言中的 for 和 foreach 語句一樣,for-of 要求被遍歷的物件實現特定的方法。所有的 Array、Map 和 Set 物件都有一個共性,那就是他們都實現了一個迭代器(iterator)方法,那麼對於其他物件呢?不著急,也可以自己實現一個迭代器方法,這就好比你對一個物件進行toString操作,來告知js如何將一個物件轉換成字串,也可以為任意物件實現一個ObjectSymbol.iterator方法,來告訴js如何去遍歷這個物件。 看到這裡,大腦裡也許會想,[Symbol.iterator] 這個語法到底是什麼鬼,是什麼意思,ES 標準委員會完全可以將該方法命名為 iterator(),但是,現有物件中可能已經存在名為“iterator”的方法,這將導致程式碼混亂,違背了最大相容性原則。所以,標準委員會引入了 Symbol,而不僅僅是一個字串,來作為方法名。 一個擁有 Symbol.iterator 方法的物件被認為是可遍歷的(iterable)。

迭代器物件

一般情況下,我們不需要從0開始實現一個迭代器物件,現在我們來看看,迭代器具體是什麼樣的, 就拿for-of語句來說吧,,它首先呼叫被遍歷集合物件的 Symbol.iterator 方法,該方法返回一個迭代器物件,迭代器物件是可以擁有.next()方法的任何物件,然後,在 for-of 的每次迴圈中,都將呼叫該迭代器物件上的 .next 方法,看下面一個簡單的迭代器物件生成方法

var Itera = {
  [Symbol.iterator]: function () {
    return this;
  },
  next: function () {
    return {done: false, value: 0};
  }
};
複製程式碼

在上面程式碼中,每次呼叫.next()方法都返回了同一個結果,該結果一方面告知for-of語句迴圈還沒有結束,另一方面告知for-of本次迴圈的值是0,這也就意味著上面方法是一個死迴圈,當然了,一個典型的迭代器絕對不會這麼簡單的 在ES6中,迭代器通過.done和.value這兩個屬性來標識每次的遍歷結果,這就是迭代器的設計原理 現在我們知道了for-of的一些細節,那麼我們可以簡單的重寫語句

for (VAR of ITERABLE) {
  程式碼內容
}
複製程式碼

上面只是一個語義化的實現,使用一些底層方法和幾個臨時變數

var iterator = ITERABLE[Symbol.iterator]();
var result = iterator.next();
while (!result.done) {
  VAR = result.value;
  STATEMENTS
  result = iterator.next();
}
複製程式碼

上面程式碼並沒有涉及到如何呼叫.return()方法,我們可以新增相應的方法去處理。for-of 語句使用起來非常簡單,但在其內部有非常多的細節值得我們去深入研究。

結束

ES6裡面新增了很多很好玩又實用的特性,都值得我們去學習,for-of只是其中的一部分,還有更多的新特性可以去學習,今天就講到這了。

相關文章