深⼊解读JavaScript中的Iterator和for-of循环
如何遍历⼀个数组的元素?在 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 字符的集合:
它还适⽤于 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]);
}
内部原理
“好的艺术家复制,伟⼤的艺术家偷窃。” — 巴勃罗·毕加索
js原型和原型链的理解
被添加到 ES6 中的那些新特性并不是⽆章可循,⼤多数特性都已经被使⽤在其他语⾔中,⽽且事实也证明这些特性很有⽤。
就拿 for-of 语句来说,在 C++、JAVA、C# 和 Python 中都存在类似的循环语句,并且⽤于遍历这门语⾔和其标准库中的各种数据结构。
与其他语⾔中的 for 和 foreach 语句⼀样,for-of 要求被遍历的对象实现特定的⽅法。所有的 Array、Map 和 Set 对象都有⼀个共性,那就是他们都实现了⼀个迭代器(iterator)⽅法。
那么,只要你愿意,对其他任何对象你都可以实现⼀个迭代器⽅法。
这就像你可以为⼀个对象实现⼀个 String()⽅法,来告知 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 = $();
while (!$result.done) {
VAR = $result.value;
STATEMENTS
$result = $();
}
var $iterator = ITERABLE[Symbol.iterator]();
var $result = $();
while (!$result.done) {
VAR = $result.value;
STATEMENTS
$result = $();
}
上⾯代码并没有涉及到如何调⽤ .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}