首頁  >  文章  >  web前端  >  深入解讀JavaScript中的Iterator與for-of循環_基礎知識

深入解讀JavaScript中的Iterator與for-of循環_基礎知識

WBOY
WBOY原創
2016-05-16 15:48:471068瀏覽

如何遍歷一個陣列的元素?在 20 年前,當 JavaScript 出現時,你或許會這樣做:

for (var index = 0; index < myArray.length; index++) {
 console.log(myArray[index]);
}
 
for (var index = 0; index < myArray.length; index++) {
 console.log(myArray[index]);
}

自從 ES5 開始,你可以使用內建的 forEach 方法:

JavaScript
myArray.forEach(function (value) {
 console.log(value);
});
 
myArray.forEach(function (value) {
 console.log(value);
});

程式碼更為精簡,但有一個小缺點:不能使用 break 語句來跳出循環,也不能使用 return 語句來從閉包函數中傳回。

如果有 for- 這種語法來遍歷陣列就會方便很多。

那麼,使用 for-in 呢?

for (var index in myArray) { // 实际代码中不要这么做
 console.log(myArray[index]);
}

 
for (var index in myArray) { // 实际代码中不要这么做
 console.log(myArray[index]);
}

這樣不好,因為:

    上面程式碼中的 index 變數將會是 "0"、"1"、"3" 等這樣的字串,而並不是數值類型。如果你使用字串的 index 去參與某些運算("2" 1 == "21"),運算結果可能會不符合預期。
不僅數組本身的元素將被遍歷到,那些由使用者添加的附加(expando)元素也將被遍歷到,例如某數組有這樣一個屬性myArray.name,那麼在某次循環中將會出現index="name " 的情況。而且,連數組原型鏈上的屬性也可能遍歷到。
    最不可思議的是,在某些情況下,上面程式碼將會以任意順序去遍歷數組元素。

簡單來說,for-in 設計的目的是用於遍歷包含鍵值對的對象,對數組並不是那麼友好。
強大的 for-of 迴圈

我記得上次我提到過,ES6 並不會影響現有JS 程式碼的正常運行,已經有成千上萬的Web 應用程式都依賴for-in 的特性,甚至也依賴for-in 用於陣列的特性,所以從來沒有人提出「改善」現有for-in 語法來修復上述問題。 ES6 解決這個問題的唯一方法是引入新的迴圈遍歷語法。

這就是新的語法:

for (var value of myArray) {
 console.log(value);
}
 
for (var value of myArray) {
 console.log(value);
}

透過介紹上面的 for-in 語法,這個語法看起來並不是那麼令人印象深刻。後面我們將詳細介紹for-of 的奇妙之處,現在你只需要知道:

  •     這是遍歷陣列最簡單直接的方法
  •     避免了所有 for–in 語法存在的坑
  •     與 forEach() 不同的是,它支援 break、continue 和 return 語句。

for–in 用來遍歷物件的屬性。

for-of 用來遍歷資料 — 就像陣列中的元素。

然而,這還不是 for-of 的所有特性,下面還有更精彩的部分。
支持 for-of 的其他集合

for-of 不只是為數組設計,還可以用於類別數組的對象,例如 DOM 對象的集合 NodeList。

也可以用於遍歷字串,它將字串看成是 Unicode 字元的集合:

201572885849513.jpg (423×77)

它也適用於 Map 和 Set 物件。

也許你從未聽說過 Map 和 Set 對象,因為它們是 ES6 中的新對象,後面將有單獨的文章去詳細介紹它們。如果你在其他語言中使用過這兩個對象,那就簡單多了。

例如,可以用一個 Set 物件來對陣列元素去重:

JavaScript
// make a set from an array of words
var uniqueWords = new Set(words);
 
// make a set from an array of words
var uniqueWords = new Set(words);

當你得到一個 Set 物件後,你很可能會去遍歷該對象,這很簡單:

for (var word of uniqueWords) {
 console.log(word);
}
 
for (var word of uniqueWords) {
 console.log(word);
}

Map 物件由鍵值對構成,遍歷方式略有不同,你需要用兩個獨立的變數來分別接收鍵和值:

for (var [key, value] of phoneBookMap) {
 console.log(key + "'s phone number is: " + value);
}
 
for (var [key, value] of phoneBookMap) {
 console.log(key + "'s phone number is: " + value);
}

到目前為止,你已經知道:JS 已經支援一些集合對象,而且後面將會支援更多。 for-of 語法正是為這些集合物件而設計。

for-of 不能直接用來遍歷物件的屬性,如果你想遍歷物件的屬性,你可以使用for-in 語句(for-in 就是用來幹這個的),或者使用下面的方式:

// dump an object's own enumerable properties to the console
for (var key of Object.keys(someObject)) {
 console.log(key + ": " + someObject[key]);
}

 
// dump an object's own enumerable properties to the console
for (var key of Object.keys(someObject)) {
 console.log(key + ": " + someObject[key]);
}

内部原理

    “好的艺术家复制,伟大的艺术家偷窃。” — 巴勃罗·毕加索

被添加到 ES6 中的那些新特性并不是无章可循,大多数特性都已经被使用在其他语言中,而且事实也证明这些特性很有用。

就拿 for-of 语句来说,在 C++、JAVA、C# 和 Python 中都存在类似的循环语句,并且用于遍历这门语言和其标准库中的各种数据结构。

与其他语言中的 for 和 foreach 语句一样,for-of 要求被遍历的对象实现特定的方法。所有的 Array、Map 和 Set 对象都有一个共性,那就是他们都实现了一个迭代器(iterator)方法。

那么,只要你愿意,对其他任何对象你都可以实现一个迭代器方法。

这就像你可以为一个对象实现一个 myObject.toString() 方法,来告知 JS 引擎如何将一个对象转换为字符串;你也可以为任何对象实现一个 myObject[Symbol.iterator]() 方法,来告知 JS 引擎如何去遍历该对象。

例如,如果你正在使用 jQuery,并且非常喜欢用它的 each() 方法,现在你想使所有的 jQuery 对象都支持 for-of 语句,你可以这样做:

// Since jQuery objects are array-like,
// give them the same iterator method Arrays have
jQuery.prototype[Symbol.iterator] =
 Array.prototype[Symbol.iterator];

 
// Since jQuery objects are array-like,
// give them the same iterator method Arrays have
jQuery.prototype[Symbol.iterator] =
 Array.prototype[Symbol.iterator];

你也许在想,为什么 [Symbol.iterator] 语法看起来如此奇怪?这句话到底是什么意思?问题的关键在于方法名,ES 标准委员会完全可以将该方法命名为 iterator(),但是,现有对象中可能已经存在名为“iterator”的方法,这将导致代码混乱,违背了最大兼容性原则。所以,标准委员会引入了 Symbol,而不仅仅是一个字符串,来作为方法名。

Symbol 也是 ES6 的新特性,后面将会有单独的文章来介绍。现在你只需要知道标准委员会引入全新的 Symbol,比如 Symbol.iterator,是为了不与之前的代码冲突。唯一不足就是语法有点奇怪,但对于这个强大的新特性和完美的后向兼容来说,这个就显得微不足道了。

一个拥有 [Symbol.iterator]() 方法的对象被认为是可遍历的(iterable)。在后面的文章中,我们将看到“可遍历对象”的概念贯穿在整个语言中,不仅在 for-of 语句中,而且在 Map和 Set 的构造函数和析构(Destructuring)函数中,以及新的扩展操作符中,都将涉及到。
迭代器对象

通常我们不会完完全全从头开始去实现一个迭代器(Iterator)对象,下一篇文章将告诉你为什么。但为了完整起见,让我们来看看一个迭代器对象具体是什么样的。(如果你跳过了本节,你将会错失某些技术细节。)

就拿 for-of 语句来说,它首先调用被遍历集合对象的 [Symbol.iterator]() 方法,该方法返回一个迭代器对象,迭代器对象可以是拥有 .next 方法的任何对象;然后,在 for-of 的每次循环中,都将调用该迭代器对象上的 .next 方法。下面是一个最简单的迭代器对象:

var zeroesForeverIterator = {
 [Symbol.iterator]: function () {
 return this;
 },
 next: function () {
 return {done: false, value: 0};
 }
};
 
var zeroesForeverIterator = {
 [Symbol.iterator]: function () {
 return this;
 },
 next: function () {
 return {done: false, value: 0};
 }
};

在上面代码中,每次调用 .next() 方法时都返回了同一个结果,该结果一方面告知 for-of语句循环遍历还没有结束,另一方面告知 for-of 语句本次循环的值为 0。这意味着 for (value of zeroesForeverIterator) {} 是一个死循环。当然,一个典型的迭代器不会如此简单。

ES6 的迭代器通过 .done 和 .value 这两个属性来标识每次的遍历结果,这就是迭代器的设计原理,这与其他语言中的迭代器有所不同。在 Java 中,迭代器对象要分别使用 .hasNext()和 .next() 两个方法。在 Python 中,迭代器对象只有一个 .next() 方法,当没有可遍历的元素时将抛出一个 StopIteration 异常。但从根本上说,这三种设计都返回了相同的信息。

迭代器对象可以还可以选择性地实现 .return() 和 .throw(exc) 这两个方法。如果由于异常或使用 break 和 return 操作符导致循环提早退出,那么迭代器的 .return() 方法将被调用,可以通过实现 .return() 方法来释放迭代器对象所占用的资源,但大多数迭代器都不需要实现这个方法。throw(exc) 更是一个特例:在遍历过程中该方法永远都不会被调用,关于这个方法,我会在下一篇文章详细介绍。

现在我们知道了 for-of 的所有细节,那么我们可以简单地重写该语句。

首先是 for-of 循环体:

for (VAR of ITERABLE) {
 STATEMENTS
}
 
for (VAR of ITERABLE) {
 STATEMENTS
}

这只是一个语义化的实现,使用了一些底层方法和几个临时变量:

var $iterator = ITERABLE[Symbol.iterator]();
var $result = $iterator.next();
while (!$result.done) {
 VAR = $result.value;
 STATEMENTS
 $result = $iterator.next();
}

 
var $iterator = ITERABLE[Symbol.iterator]();
var $result = $iterator.next();
while (!$result.done) {
 VAR = $result.value;
 STATEMENTS
 $result = $iterator.next();
}

上面代码并没有涉及到如何调用 .return() 方法,我们可以添加相应的处理,但我认为这样会影响我们对内部原理的理解。for-of 语句使用起来非常简单,但在其内部有非常多的细节。
兼容性

目前,所有 Firefox 的 Release 版本都已经支持 for-of 语句。Chrome 默认禁用了该语句,你可以在地址栏输入 chrome://flags 进入设置页面,然后勾选其中的 “Experimental JavaScript” 选项。微软的 Spartan 浏览器也支持该语句,但是 IE 不支持。如果你想在 Web 开发中使用该语句,而且需要兼容 IE 和 Safari 浏览器,你可以使用 Babel 或 Google 的 Traceur 这类编译器,来将 ES6 代码转换为 Web 友好的 ES5 代码。

对于服务器端,我们不需要任何编译器 — 可以在 io.js 中直接使用该语句,或者在 NodeJS 启动时使用 --harmony 启动选项。

{done: true}

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn