Es6 in Depth: Iterators and the For-of Loop

ES6 In Depth: Iterators and the for-of loop

【深入ES6】是深入了解 ECMAScript 标准中第6版本的JS(Javascript)语言新特性的系列文章,ES6是缩写。

你是怎么循环处理数组中的元素的?20年前的JS,你可能会如下:

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

ES5中,你可以使用内置的方法:

1
2
3
myArray.forEach(function (value) {
  console.log(value);
});

这样子会简单些,但是这里有一个缺点:你不能使用break跳出循环,或者你也不能使用return跳出整个函数。

如果使用for-循环语法,显得更为好些。

那么,for-in循环怎么样呢?

1
2
3
for (var index in myArray) { // don't actually do this
  console.log(myArray[index]);
}

这是个坏主意,因为:

  • 代码中的index是字符串,”0”,”1”,”2”等,并不是真正的数值。因此,你可能会得到不想要的结果,如 “2”+1=”21”,这是十分不方便的。
  • 此循环中的主体部分,不仅仅数组的元素的执行,同时,其他人增加的属性也会被执行。例如,如果你的数组中包含可访问的属性myArray.name,那么这个循环将会执行额外的一次,此时的 index == "name"。甚至,数组原型链中的属性也会被访问。
  • 更加令人无语的是,要某些环境中,以上代码的执行顺序是不确定的。

简单来说,for-in是为以字符串为关键字的旧Objects 来设计的。对于,数组来说,这方法并不好。

强大的 for-of 循环

上周,我已经提到了ES6不会影响到你现有的JS代码。好了,现在成千上万的网站依赖了 for-in,甚至有可能已经使用在数组(Array)中。然而,当数组中使用for-in时,并没有什么好方法来解决它的问题。ES6的唯一方法是增加一些新的循环语法来解决这些问题。

在这里,

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

相较于已有的其它结构,它是不是也很诱人?好了,我们将会看到for-of优雅的使用技巧。现在,先说明一下:

  • 更加精简。循环中直接使用数组元素的语法;
  • 它避免了for-in的陷阱;
  • 不像forEach,它可以使用breakcontinuereturn

for-in循环会对处理对象的属性。

for-of循环会处理数据 – 如数组中的值。

但这并不是其所有的特性。

for-of 对其它集合的支持

for-of 不仅仅作用于数组,它还可用于类数组的对象,如 DOM NodeList

它还可用于字符串,将字符串看成是 Unicode 字符序列:

1
2
3
for (var chr of "  ") {
  alert(chr);
}

它还可用于MapSet对象中。

噢,对不起。你还没有听说过MapSet对象?好吧,它们是ES6新添加的对象。我们将会在以后用一整篇文章来对它们的主要特点进行说明。如果你已经在其它编程语言中碰到过它们,应该就没什么觉得特别的了。

例如,一个Set对象有利于消除重复数据:

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

当你拥有一个Set时,你可以这样子很简单地循环其内容:

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

Map稍微有点不同:里面的数据是 key-value 对,所以,你很可能去结构化(destructuring)来打开 key 和 value,并赋予两个不同的变量:

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

去结构化是ES6的特性,我们也将会在以后的文章中进行说明,此处就写到这里。

现在,你应该有个印象:JS 拥有些新的不一样的集合类,甚至是它们的使用方法。for-of被设计成一个吃苦耐劳的老马的循环表达式,来操作这些集合。

for-of 不能用于原有的对象中,但是,如果你想循环其对象属性时,你可能使用for-in(这就是其产生的原因),或者使用内置的 Object.keys()

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

掀开面纱(Under the hood)

“Good artists copy, great artists steal.” —- Pablo Picasso

ES6主要推行的是现在JS语言没有的一些新添加的特性,大多数已经在其它语言中使用并证明是可用的。

for-of循环类似于 C++、Java、C#、Python中的循环表达式,它可以处理JS语言及其标准库中的多种数据结构。但是,它还进行些扩展。

就像这些其它语言中的for/foreach表达式,只要相应方法可调用,for-of就能使用。我们之前讨论到的 Array``Map``Set及其它的对象,都拥有一个迭代方法。

其它类型的对象,都可拥有迭代方法,任何对象都可以。

你可以添加myObject.toString()方法给任何对象,然后JS就知道将对象转化成为字条串。类似这样的,你可以添加myObject[Symbol.iterator]()方法给任何对象,然后JS就知道如何循环这对象了。

例如,假定你正在使用 jQuery,而且非常喜欢使用.each()方法,而你你想jQuery对象能够使用for-of方法,那么可以这样子:

1
2
3
// 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, 而不是一个字符串,也不是仅仅的一个方法。

符号(Symbol)是ES6新添加的特性,我们将会讨论到它–你猜什么时候–在之后的一篇文章中。现在,你只需要知道新标准可以定义新形式(brand-new)的符号,如 Symbol.iterator,它保证肯定是不会与现有的代码有冲突。这一做法从语法上看显得很怪异,但是,这样可以以较小的代价来得到这个通用的新特性和很好地向下兼容。

一个对象拥有[Symbol.iterator]()方法时,称为iteratable。在以后的周里,我们将会看到JS语言的iteratable的用法,不仅仅在for-of,同时还在MapSet的构造函数中,去结构化时的分配(destructuring assignment),还有新的应用广泛的操作符。

迭代对象

现在,你不 一定要对你自己的对象实现为一个迭代器,它可能是有错误的,这也是为什么我们将在下周在说明的原因。但是,为了完整性,让我们看一下一个迭代器是怎么样的。(如果你跳过这一部分,你很可能会错过些易忽略的(crunchy)技术细节。)

一个for-of循环在集合中首先会调用[Symbol.iterator]()方法,它会返回一个新的迭代器对象。这个迭代器对象可以是做任意的拥有.next()方法的对象。当for-of循环每一次执行时,它会反复地调用这个方法。例如,这里有个我认为是最为简单的迭代器对象:

1
2
3
4
5
6
7
8
var zeroesForeverIterator = {
  [Symbol.iterator]: function () {
    return this;
  },
  next: function () {
    return {done: false, value: 0};
  }
};

每次.next()调用时,它会返回相同的结果,告诉for-of: a). 我们还没有停止这个迭代器,b). 下个值是0,这表示for (value of zeroesForeverIterator) {}将是个无限的循环。好吧,常用的迭代器是没必要这么繁琐的。

这个迭代器设计,包含有 .done.value属性,很明显地不同于其它语言的迭代器。在Java中,迭代器有两个不同的.hasNext().next()方法。在Python中,它只有一个.next()方法,当没有更多值时会抛出StopIteration。但是,从根本上说,这三种设计都返回相同的信息。

迭代器对象也可以实现可选的return().throw(exc)方法。当for-of出现错误或者调用breakreturn表达式时,会提前退出,这时调用.return()方法。如果迭代器需要做些清理工作或者清空正式使用的资源,可以对.return()方法运行实现,多数迭代器对象是不需要实现它的。.throw(exc)是一种特殊的情况,for-of是不能调用这方法的。但是,我们将在下周看到更多相关的信息。

现在,我们知道了迭代吕所有的详细信息,我们可以根据以下的方法来写简单的for-of循环:

首先for-of循环:

1
2
3
for (VAR of ITERABLE) {
  STATEMENTS
}

这里是个粗糙的等价物(示例),使用下面的方法和些临时的变量:

1
2
3
4
5
6
7
var $iterator = ITERABLE[Symbol.iterator]();
var $result = $iterator.next();
while (!$result.done) {
  VAR = $result.value;
  STATEMENTS
  $result = $iterator.next();
}

这代码并没有说明.return()是怎么处理的,我们可以添加上去,但是,我认为这样子会让人难以理解它是怎么工作的,还不如简明点好。for-of很易用,但其在后面仍然有很多知识。

什么时候我可以用上

for-of循环在现在所有的发行版本都是支持的,如果你在chrome中打开chrome://flags并启动”Experimental JavaScript”,chrome也会支持。微软Spartan浏览器也支持,但固守的IE版本(in the shiping versions of IE)是不支持的。如果你希望使用新的语法在网页开发中,但是又需要支持IE和Safari,你可以使用如Babel或Google的Traceur,来将ES6代码转化为现在对Web更为友好的ES5。

在服务器端,你不需要转化器,你可以在io.js直接使用for-of,或者在带参--harmony的Node。

好了,今天我们就到这,但是for-of还没有。

在ES6中,有更多的对象可以漂亮地使用for-of,我之所以没有提到,是因为那是下周文章的主题。我认为这是在ES6最为魔法的地方,如果你没有遇到过,比如在Python和C#中,你开始会有些芥蒂。但是,写一个迭代器是很简单的,对重构是很有用的。同时,无论是在浏览器还是服务器中,它可能还会改变我们写异步代码的方式。所以,在下周加入我们吧,我们会深入ES6的生成器(generators)。

Comments(回复)

Brett Zamir wrote on April 30th, 2015 at 17:31:

arr.some() 方法,可以中断循环和跳过循环。

跳过循环:

1
2
3
4
5
6
arr.some(function(e){
  if(e%2==0){return false;};
  console.log(e);
})

# 输出 1, 3, 返回值为false

中断循环:

1
2
3
4
5
6
arr.some(function(e){
  if(e%2==0){return true;};
  console.log(e);
})

# 输出 1, 返回值为true

Andrea Giammarchi wrote on May 1st, 2015 at 07:22:

Andrea 提出个暴力的方法,直接重写数组,来结束整个循环,如下:

1
2
3
4
5
arr = [1,2,3];
arr.forEach(function (v,i,a) {
  console.log(i, v);
  a.length = 0;
});

注意,直接会影响循环的数组,运行此代码后,arr = []

其实,JS原有的forEach通过return可能跳过本次循环的,但是不能直接结束循环。

Comments