你是如何遍历数组中的元素的?20年前JavaScript刚进入视野时,你应该是这样写的:
- for (var index = 0; index < myArray.length; index++) {
- console.log(myArray[index]);
- }
直到ES5中原生JavaScript中添加了forEach方法:
- myArray.forEach(function (value) {
- console.log(value);
- });
语法上简洁了一些,但是它有一个小小的不足:你不能用break语句跳出循环且不能在这个封闭的函数内使用return语句。
如果有一个简单的for-loop语法来遍历数组就好了。
使用一个for-in循环怎么样?
- for (var index in myArray) { // don't actually do this
- console.log(myArray[index]);
- }
我用几个理由来说明这并不是一个好主意:
- 数组的索引值index是String类型的“0”,“1”,“2”等等,而不是Number类型。当你进行算术运算时(“2”+1==“21”)也许并不是你期望的结果,所以运算前需要类型转换,这很不方便。
- 循环体不仅会遍历数组的元素,甚至连expando属性也遍历出来了。举个例子,如果你的myArray数组中有一个叫做name的属性,遍历时就将 index ==”name”也遍历出来,这样就多了一次执行。即时这些属性在数组的原型链上是可直接访问的。
- 最让人无语的是,在某些情况下,这段代码在遍历数组元素时顺序是任意的。
总而言之,for-in语法是被设计来遍历普通的“键值对”对象的,不适合用在数组上。
强大的for-of循环
即使在遍历数组的时候,现在的成千上万的网站也使用了for-in循环。所以“修复”for-in让它更适用于数组是有必要的。ES6来解决这个问题的唯一途径是新增一个新的遍历语法。这个语法就是for-of循环:
新语法如下:
- for (var value of myArray) {
- console.log(value);
- }
恩?!从构建上来说好像并没什么改变,事实如此吗?当然不是,我们来看看for-of的葫芦里究竟卖的什么药。首先,只需要注意这几点:
- 这是目前遍历数组最简洁和直接的语法;
- 它避免了for-in的所有缺陷;
- 与forEach()不一样,它支持break,continue和return。
for-in循环用于遍历对象属性。
for-of循环用于遍历数据——比如数组中单值。
其它集合也支持for-of
for-of循环不仅仅是为遍历数组而设计的。基本上所有类数组对象都适用,比如DOM NodeListS。
也能用在字符串上,它将字符串当做一个Unicode字符序列:
- for(var chr of "abcd"){
- alert(chr)
- }
它也能用在Map和Set对象上。
哦,不好意思,你没听说过Map和Set?没关系,他们是出现在ES6中的新成员。有机会我们会写个完整的关于它的文章。如果你使用过其它编程语言中的maps和sets,那么你也不会有陌生感。
例如,一个set对象使用于排除重复项:
- // make a set from an array of words
- var uniqueWords = new Set([1,2,3]);
如果你想遍历你的set,很简单:
- for (var word of uniqueWords) {
- console.log(word);
- }
Map有一点不同:它里面的数据由键值对组成,所以你需要使用destructuring将“键”和“值”解构为两个独立的变量:
- for (var [key, value] of phoneBookMap) {
- console.log(key + "'s phone number is: " + value);
- }
Destructuring(解构)也是ES6的新特性,在未来博客中会有很多关于它的文章。
目前为止,你可以这样理解:JS已经有了几个不同的集合类,而且更多的集合类正在被添加进来。for-of循环语句的设计初衷就是适用于所有这些集合类。
for-of并不能用于普通的旧对象。如果你想要遍历对象的所有属性,可以使用for-in,也可以通过Object.keys(object)将对象的所有属性以数组形式返回后再使用for-of。
- // dump an object's own enumerable properties to the console
- for (var key of Object.keys(someObject)) {
- console.log(key + ": " + someObject[key]);
- }
深入理解
“能工摹形,巧匠窃意。”——巴勃罗·毕加索
JavaScript在ES6中所新增的特性并不是凭空而来,大多数都是借鉴于其它优秀的语言。
以for-of循环伪例,与C++、Java、C#和Python的循环语句非常类似。和它们一样,支持该种语言提供的多种数据解构和标准库。但是它也是该种语言的一个扩展点。
就像for/foreach语句在其它语言中一样,for-of的执行完全靠方法调用。像Arrays,Maps,Sets等我们提到过的对象都有一个共同点就是它们都有一个遍历的方法。
其它类型的对象也都可有一个遍历方法:任何对象都可以。
就像你可以对任何一个对象添加方法myObject.toString()让JS知道如何将对象转换为字符串一样,你也可以对任何对象添加方法myObject.toString()来告诉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];
好吧,我知道你会觉得[Symbol.iterator] 这样的语法看起来很奇怪。它是怎么执行的呢?使用方法名就可以了。标准委员会刚刚将这个方法命名为.iterator(),但是你已存在的代码中可能已经有了叫做.iterator的方法,那会造成命名冲突,让人傻傻分不清。因此所有标准库将其封装进了symbol,而不是使用简单的用字符串来直接命名。
Symbols是ES6的新特性,我们将在以后的博客中讨论它。目前,你所需要知道的是现在标准定义了一个全新的symbol,比如Symbol.iterator,为了保证与已存在的代码不存在命名冲突,所以这个代价就是语法看起来有点奇怪。
为了这个优秀的新特性的向后兼容性,这点小代价也就微不足道了。
迭代器对象
从现在开始你再也没有必要为自己写一个迭代器对象了,这个我们在下篇文章中再来讨论。但是出于完整性的考虑,让我们先来看看一个迭代器对象是什么样子的。(如果你跳过这一节,你会错过很多有趣的技术细节哟)。
for-of循环开始于对集合的[Symbol.iterator]()方法的调用。它会返回一个新的迭代器对象。任意一个有.next()方法的对象都可以被称作迭代器对象;每次执行进入循环时,for-of方法将会用.next()方法。例如,下面是一个我所能想到的最简单的迭代器构造:
- var zeroesForeverIterator = {
- [Symbol.iterator]: function () {
- return this;
- },
- next: function () {
- return {done: false, value: 0};
- }
- };
每次当.next()方法被调用的时候,它会返回相同的结果,告诉for-of循环:(1)我们还没结束迭代;(2)下一个值是0。这意味着,(value of zeroesForeverIterator) {}将是一个无线循环。当然,一个真正的迭代器并不会这么简单。
迭代器的设计,伴随着.done和.value属性,从表面上来看似乎和其它语言中的迭代器不太一样。在Java中,迭代器将.hasNext()和.next()区分为两个方法。在Python中,它只有一个.next()方法,当没有下一个值时会抛出StopIteration 。但是从根本上来说,这三种方法返回同样的信息。
迭代器也可以实现一些可选方法,比如.return()和throw(ext)。在for-of循环中,当遇到异常或者break和return语句时可以调用.return()方法提前退出循环。迭代器可以通过实现.return()方法来清空变量或释放当前资源,大多数迭代器对象是使用不到这一点的。.throw(exc)是一个特殊的例子:for-of完全使用不到它,我们下次再来讨论。
现在我们已经了解了所有的基本细节,我们可以写一个简单的循环并重写它的底层方法调用部分。
先写一个for-of循环:
- for (VAR of ITERABLE) {
- STATEMENTS
- }
下面这段代码使用简单的底层方法和几个简单的变量来实现同样的功能:
- var $iterator = ITERABLE[Symbol.iterator]();
- var $result = $iterator.next();
- while (!$result.done) {
- VAR = $result.value;
- STATEMENTS
- $result = $iterator.next();
- }
这段代码并没有体现出.return()操作。我们可以添加进来,但是我认为认清它的执行过程比阐明它更重要。for-of的使用起来很简单,但有很多看不见幕后的工作。
我什么时候才能使用它?
当前所有的Firefox releases版本都支持for-of循环。如果你想在Chrome中使用,到chrome://flags设置“Experimental JavaScript”为“开启”即可。微软的Spartan浏览器支持它,但是IE不支持。如果你想要在Web中使用这些新语法且不用考虑支持IE和Safari,你可以使用Babel或者谷歌的Traceur这样的编译器将你的ES6代码转换成兼容性友好的ES5。
在服务端,你不需要一个编译器——你可以在io.js(基于Node,是一个不错的选择)中使用for-of。
(更新:在Chrome中默认是禁用的,这个被我忽视掉了,感谢Oleg 指出。)
讲完啦!
我们今天的计划都完成了,但是我们对for-of循环的学习还没结束。
ES6中还有一个和for-of完美结合的新对象。我之所以没提到它是因为它是我们下次的主题。
我认为它是ES6中最神奇的新特性。如果你之前没在像Python和C#这样的语言中使用过它,一开始它可能会让你感到难以置信。无论在客户端还是服务端,这是写一个构造器最简单方法,对于重构很有用,它有可能会改变我们写异步代码的方式习惯。