ES6 深入:迭代器和 for-of 循环

ES6 深入 是关于 ECMAScript 标准第六版(简称 ES6)中添加的 JavaScript 编程语言新功能的系列文章。

你如何遍历数组中的元素?二十年前 JavaScript 推出的时候,你可以这样做

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

自 ES5 以来,你可以使用内置的 forEach 方法

<pre>
myArray.forEach(function (value) {
console.log(value);
});
</pre>

});

这代码稍微短一些,但有一个小缺点:你不能使用 break 语句退出此循环,也不能使用 return 语句从封闭函数中返回。

如果有一个 for 循环语法专门用来遍历数组元素,那该多好。

<pre>
如何使用 forin 循环?
console.log(myArray[index]);
}
</pre>

for (var index in myArray) { // 不要这样做

  • }
  • 这样做是不可取的,原因如下:
  • 此代码中分配给 index 的值是字符串 "0""1""2" 等等,而不是实际数字。由于你可能不想要字符串运算("2" + 1 == "21"),所以这样做充其量是不方便的。

循环体不仅会为数组元素执行,还会为其他人可能添加的任何其他 扩展 属性执行。例如,如果你的数组有一个可枚举属性 myArray.name,则此循环将多执行一次,其中 index == "name"。甚至数组原型链上的属性也会被访问。

最令人惊讶的是,在某些情况下,此代码可以以任意顺序遍历数组元素。

简而言之,forin 旨在用于具有字符串键的普通 Object。对于 Array 来说,它不是很好用。

强大的 for-of 循环

<pre>
还记得上周我说过 ES6 不会破坏你已经编写的 JS 代码吗?好吧,数百万个网站依赖于 forin 的行为,是的,即使是在数组上的行为。因此,绝不可能“修复”forin 使其在用于数组时更方便。ES6 改善这种情况的唯一途径是添加某种新的循环语法。
console.log(value);
}
</pre>

它就是这个样子

  • for (var value of myArray) {
  • }
  • 嗯,经过了那么多铺垫,它看起来并不那么令人印象深刻,是吗?好吧,我们会看看 forof 是否有什么巧妙的技巧。现在,只需注意

这是迄今为止最简洁、最直接的遍历数组元素的语法

它避免了 forin 的所有陷阱

forEach() 不同,它支持 breakcontinuereturn

forin 循环用于遍历对象属性。

forof 循环用于遍历数据,比如数组中的值。

但不止这些。

<pre>
其他集合也支持 for-of
forof 不仅适用于数组。它还适用于大多数类似数组的对象,比如 DOM NodeList
}
</pre>

它还适用于字符串,将字符串视为 Unicode 字符序列

for (var chr of "😺😲") {

alert(chr);

<pre>
}
它还适用于 MapSet 对象。
</pre>

哦,抱歉。你以前没听说过 MapSet 对象?好吧,它们是 ES6 中的新功能。我们会在以后的文章中专门介绍它们。如果你在其他语言中使用过映射和集合,那么你不会感到陌生。

<pre>
例如,Set 对象适用于消除重复
// 从单词数组创建集合
}
</pre>

var uniqueWords = new Set(words);

<pre>
创建了 Set 后,你可能想遍历其内容。很简单
for (var word of uniqueWords) {
}
</pre>

console.log(word);

}

Map 有点不同:它内部的数据由键值对组成,因此你需要使用解构将键和值解包到两个单独的变量中

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

解构是 ES6 中的另一个新功能,也是以后博客文章的热门话题。我应该把这些记下来。

现在,你应该明白了:JS 已经有很多不同的集合类,并且还有更多正在开发中。forof 被设计为用于所有这些集合的通用循环语句。

forof 适用于普通 Object,但是如果你想遍历对象的属性,可以使用 forin(这就是它的用途)或内置的 Object.keys()

// 将对象的自身可枚举属性转储到控制台

for (var key of Object.keys(someObject)) {

console.log(key + ": " + someObject[key]);

}

幕后

<pre>
“好的艺术家会模仿,伟大的艺术家会偷窃。” — 巴勃罗·毕加索
ES6 中一个贯穿始终的主题是,添加到语言中的新功能并非凭空出现。大多数功能都曾在其他语言中尝试过并证明了其有用性。
例如,forof 循环类似于 C++、Java、C# 和 Python 中的类似循环语句。与它们一样,它适用于语言及其标准库提供的多种不同的数据结构。但它也是语言的扩展点。
与这些其他语言中的 for/foreach 语句一样,forof 完全依赖于方法调用来实现ArrayMapSet 和我们前面提到的其他对象都有一个共同点,就是它们都有一个迭代器方法。
</pre>

还有另一种对象也可以具有迭代器方法:任何你想要的

正如你可以向任何对象添加 myObject.toString() 方法,这样 JS 就会知道如何将该对象转换为字符串一样,你也可以向任何对象添加 myObject[Symbol.iterator]() 方法,这样 JS 就会知道如何遍历该对象。

例如,假设你正在使用 jQuery,虽然你非常喜欢 .each(),但你希望 jQuery 对象也支持 forof。以下是如何做到这一点

// 由于 jQuery 对象类似于数组,

// 所以给它们赋予与数组相同的迭代器方法

jQuery.prototype[Symbol.iterator] =

<pre>
Array.prototype[Symbol.iterator];
好的,我知道你在想什么。那个 [Symbol.iterator] 语法看起来很奇怪。这到底是怎么回事?这与方法的名称有关。标准委员会本可以简单地将此方法命名为 .iterator(),但是,你的现有代码中可能已经有一些对象具有 .iterator() 方法,这会造成很大的混乱。因此,标准使用符号而不是字符串作为此方法的名称。
符号是 ES6 中的新功能,我们会在以后的文章中详细介绍它们。现在,你只需要知道标准可以定义一个全新的符号,比如 Symbol.iterator,并且保证它不会与任何现有代码冲突。代价是语法有点奇怪。但这对于这个多功能的新功能和出色的向后兼容性来说是一个小代价。
},
具有 [Symbol.iterator]() 方法的对象称为可迭代对象。在接下来的几周内,我们将看到可迭代对象的概念在整个语言中都有应用,不仅在 forof 中,还包括在 MapSet 构造函数、解构赋值和新的扩展运算符中。
迭代器对象
}
};
</pre>

每次调用此.next()方法时,它都返回相同的结果,告诉for-of循环 (a) 我们尚未完成迭代;以及 (b) 下一个值为0。这意味着for (value of zeroesForeverIterator) {}将是一个无限循环。当然,典型的迭代器不会像这样简单。

这种迭代器设计,其.done.value属性,在表面上与其他语言中迭代器的运作方式不同。在 Java 中,迭代器具有独立的.hasNext().next()方法。在 Python 中,它们只有一个.next()方法,在没有更多值时抛出StopIteration。但所有这三种设计在根本上返回相同的信息。

迭代器对象还可以实现可选的.return().throw(exc)方法。如果循环由于异常或breakreturn语句而提前退出,for-of循环将调用.return()。如果迭代器需要执行一些清理操作或释放它正在使用的资源,它可以实现.return()。大多数迭代器对象不需要实现它。.throw(exc)则是一个更加特殊的情况:for-of根本不会调用它。但我们下周将更多地了解它。

现在我们已经掌握了所有细节,我们可以使用一个简单的for-of循环,并根据底层方法调用对其进行重写。

首先是for-of循环

<pre>
for (VAR of ITERABLE) {
STATEMENTS
}
</pre>

这是一个粗略的等效代码,使用底层方法和一些临时变量

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

此代码没有展示.return()是如何处理的。我们可以添加它,但我认为这会掩盖正在发生的事情,而不是阐明它。for-of易于使用,但幕后有很多工作要做。

我什么时候可以使用它?

for-of循环在所有当前的 Firefox 版本中都受支持。如果您转到chrome://flags并启用“实验性 JavaScript”,它将在 Chrome 中受支持。它也适用于微软的 Spartan 浏览器,但不能在 IE 的发布版本中使用。如果您想在网络上使用这种新语法,但需要支持 IE 和 Safari,您可以使用像Babel或 Google 的Traceur这样的编译器,将您的 ES6 代码转换为 Web 友好的 ES5 代码。

在服务器上,您不需要编译器——您今天就可以在 io.js(以及 Node,使用--harmony选项)中开始使用for-of

(更新: 此前忽略了在 Chrome 中默认情况下禁用了for-of。感谢 Oleg 在评论中指出了这个错误。)

{done: true}

呼!

好了,我们今天就到这儿了,但我们还没有完成for-of循环。

ES6 中还有一种新的对象类型,它与for-of配合得很好。我没有提到它,因为它将是下周文章的主题。我认为这个新特性是 ES6 中最神奇的东西。如果您之前没有在 Python 和 C# 等语言中遇到它,您一开始可能会觉得它令人难以置信。但它是编写迭代器最简单的方法,它在重构中很有用,并且可能会改变我们编写异步代码的方式,无论是在浏览器中还是在服务器上。所以,下周让我们深入探讨一下 ES6 生成器。

关于 Jason Orendorff

更多 Jason Orendorff 的文章…


26 条评论

  1. Brett Zamir

    很棒的文章…

    不妨提一下,Array.some 至少可以通过返回 true 来处理 Array.forEach 中缺少的“break”(而“continue”可以通过返回 false 来完成)。不幸的是,目前还没有数组扩展可以同时允许提前中断执行以及减少为一个值,从而允许“return”结果。

    就个人而言,我更喜欢数组扩展比 for-of 更容易复用(以及函数式风格)(传入函数名称比构建一个可迭代对象更容易),并且希望有一个 Object.forEach(以及 String.forEach 等等),既可以迭代普通对象的 value,又可以允许提前中断执行。

    有一个问题:for-of 迭代字符串是否确实为每个 Unicode 字符循环一次,还是为每个 Unicode 代码点循环一次?

    2015 年 4 月 30 日 下午 5:31

    1. Jason Orendorff

      > 就个人而言,我更喜欢数组扩展比 for-of 更容易复用(以及函数式风格)(传入函数名称比构建一个可迭代对象更容易)

      函数式技术很棒。数组扩展通常比过程式代码有所改进,而 for-of 不会改变这一点。

      但是,下次您使用 .reduce() 时,如果它变得很复杂,请尝试将其重写为 for-of 循环。有时最清晰的代码是直接的过程式代码,它只是简单地告诉计算机该做什么。有时,表示状态的最佳方式是变量。:)

      尤其是,一旦 for-of 被广泛支持,我不确定是否有理由使用 .forEach()。循环语法更加清晰简洁,并且与语言的其余部分更好地配合。(你没有欺骗任何人,.forEach()。我们知道你是一个过程式结构!)

      无论如何——如果你喜欢函数,过几周我会给你带来好消息。将会有包含固定点组合子的示例代码。这会很疯狂。

      > 一个问题:for-of 迭代字符串是否确实为每个 Unicode 字符循环一次,还是为每个 Unicode 代码点循环一次?

      它确实为每个 Unicode 字符循环一次。

      2015 年 5 月 1 日 上午 8:31

  2. Luke

    不错,但它会在所有浏览器中运行吗?Underscore 非常适合为循环和映射等提供更好的语法,它甚至可以在旧的 IE 中运行,例如。

    2015 年 4 月 30 日 下午 8:12

  3. Šime Vidas

    [剧透] 下周的主题是生成器。

    2015 年 4 月 30 日 下午 8:53

  4. ziyunfei

    期待下一篇文章,顺便说一句,“二十年前”,JS1.0 还没有数组。:-D

    2015 年 5 月 1 日 上午 0:46

  5. Andrea Giammarchi

    别忘了 polyfill ;-)

    https://github.com/WebReflection/get-own-property-symbols

    2015 年 5 月 1 日 上午 7:17

  6. Andrea Giammarchi

    @Brett

    > 不幸的是,目前还没有数组扩展可以同时允许提前中断执行以及减少为一个值,从而允许“return”结果。

    从技术上来说这是错误的,您可以随时从 Array 扩展方法中退出。

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

    这很脏,而且会修改数组,但至少对于运行时检查有效 ;-)

    > 以及减少为一个值,从而允许“return”结果。

    难道这不是 Array.prototype.reduce 的作用吗?

    var o = [‘a’, ‘b’, ‘a’].reduce(function (o, k) {
    o[k] = (o[k] | 0) + 1;
    return o;
    }, {});

    console.log(o); // {a: 2, b: 1}

    祝好

    2015 年 5 月 1 日 上午 7:22

  7. Daniel Herman

    很棒的文章!一个小细节——既然我们谈论的是 ES6,我觉得你的例子应该使用 `let` 而不是 `var` 来阐明 for-of 循环中的块级作用域。除此之外…<3

    2015 年 5 月 1 日 上午 8:47

  8. Igor

    什么是类数组对象的定义?

    2015 年 5 月 1 日 上午 11:15

    1. Jason Orendorff

      @Igor:哦,好问题。它指的是具有 `.length` 属性并且可以使用 `object[index]` 语法访问元素的对象。

      2015 年 5 月 1 日 下午 2:16

  9. Ken Arnold

    有没有像 Python 中的 ‘enumerate(iterable)’ 一样同时获取索引(或键)和值的惯用方法?“二十年前”的初始示例免费提供了索引,forEach 将索引作为回调的第二个参数提供,而在 CoffeeScript 中,你可以说 ‘for item, index in array’。

    (奖励:Python 的 ‘enumerate’ 接受一个可选的第二个参数,即起始索引;我发现这在迭代数组的切片时非常有用。)

    2015 年 5 月 1 日 下午 6:18

    1. Vlad

      @Ken:您可以像这样编写自己的迭代器方法
      Array.prototype.myIterator = function* (startIdx = 0) {
      while(startIdx < this.length) {
      if(this.hasOwnProperty(startIdx)) {
      yield [this[startIdx], startIdx];
      }
      startIdx++;
      }
      };

      然后在 for-of 循环中使用它
      for(var [val, idx] of [0,2,4,6,8].myIterator(1)) {
      console.log(val, idx);
      }

      2015 年 5 月 1 日 下午 10:32

      1. Jason Orendorff

        抱歉,该网站吞掉了您的缩进。我不知道如何修复它。

        我把它放到 codepen 中,这样人们就可以用它来实验:http://codepen.io/anon/pen/GJJemX

        2015 年 5 月 3 日 上午 8:54

    2. Jason Orendorff

      Ken:在 ES6 中,数组有一个 .entries() 方法,它类似于 enumerate()。它返回一个迭代器,该迭代器生成 [index, value] 对。与 for-of 一样,它在 Firefox、Chrome、io.js、Node 中实现。

      但这只解决了数组的问题。标准库也没有 Python 的 zip()。有人会很有趣地为 JS 编写一个非常棒的 itertools 库……

      2015 年 5 月 3 日 上午 9:03

      1. Siva

        感谢您撰写这篇精彩的文章… 为什么我们没有默认包含对 ‘for of’ 中索引的支持?只是想知道原因是什么。

        2015 年 5 月 4 日 上午 1:29

        1. Jason Orendorff

          值是最常见的用例,到目前为止。请注意,enumerate() 并不是 Python(或任何其他我知道的具有“foreach”循环语法的语言)的默认值。

          2015 年 5 月 4 日 下午 12:01

      2. Nick Fitzgerald

        https://github.com/fitzgen/wu.js

        我已经为 JS 编写了 itertools ;)

        2015 年 5 月 14 日 上午 5:43

  10. Martin Rinehart

    哦,天哪。不是传统的 for/in,又是它。拜托!多年来,这都是错误的,而且一直在重复。

    一如既往,在 ES6 之前,for/in 几乎在所有情况下都是遍历数组的最佳方式。让我反过来分析您的三个要点。

    您提到(第三点)for-in 可能会以乱序返回元素。但这只适用于某些古老的浏览器(当然不支持 ES6 扩展)。如果使用的是数组字面量,或者创建了一个空数组并使用 push() 插入值,那么这永远不会发生。

    其次,我编辑了 Mozilla 词汇表,将非数字命名数组下标包含在内,称为“扩展”属性,因为我知道你的意思,你的示例(“name”作为数组索引)非常清楚。如果数组中存在这样的“扩展”元素,则存在问题。循环是否应该包含或忽略它们是一个问题。For-in 会包含它们。使用一元加号将筛选出它们。真正的问题是,有人一直在进行一些非常糟糕的编码,添加了这些属性,这应该在源头被追踪并根除。使用循环遍历这些令人讨厌的元素会导致旧错误潜伏。这绝不是一个好主意。

    首先,你说的对,for/in 使用属性名称。这是循环最有效的方式,因为数组是基于属性名称构建的。(毫无疑问,JS 中的一个主要缺陷,但 ES6 没有解决它。For-of 也没有解决它。想想 WebGL,它非常重要。)说你可以用这些名称索引做一些非常愚蠢的事情,比如算术运算,是正确的。但这是否是一个实际问题?还是一个“初学 JavaScript,需要学习基础知识”的例子?

    for-in 循环是唯一一种能够正确处理稀疏数组的循环,JS 数组对象就是这种情况。在旨在处理 UI 的语言中,允许删除数组元素是强制性的。你见过多少个用(arr[index] !== undefined)代码正确保护的旧式 C 语言循环?For-in 很好地处理了已删除的元素。

    我还没有深入探索 for-of 循环,看看它是否能够正确处理神奇且模棱两可的“undefined”。(“undefined”可能是已定义属性的值,也可能表示没有通过给定名称定义的属性。)你可以将这种考虑因素添加到你的文章中。

    我已经对此进行了更详细的说明,

    http://martinrinehart.com/frontend-engineering/engineers/javascript/arrays/sparse-arrays.html

    底线是,你几乎不应该在数组上使用 C 语言风格的算术循环。For/In 几乎总是更好,我不确定 for/of 是否会有所改进。

    2015 年 5 月 3 日 下午 11:13

    1. Jason Orendorff

      感谢你的评论!我恐怕不能回答所有应该回答的问题,但只能回答其中几个。

      3. 在发布的 Firefox 中,for-in 有时会按插入顺序而不是数字顺序枚举数组元素 ID。可能只有非常稀疏的数组会受到影响,但我不确定。

      2. 我同意,扩展不应该存在于数组中,但有时会存在,例如 RegExp.prototype.exec() 返回的数组。

      1. 我认为语言陷阱是一个实际问题。无论开发者教育程度如何,都会偶尔犯错,即使是“愚蠢”的错误也会浪费时间。

      关于速度的另一个说明。如今的数组元素*不再*以属性表的内部方式表示。现在的内部表示非常类似数组。因此,在数组上,C 语言风格的循环速度可以比 for-in 快 400 倍!(for-of 目前还没有达到最佳速度,但它已经比 for-in 快 20 倍,我们仍在对其进行优化。)

      2015 年 5 月 4 日 下午 12:53

  11. Phil Stricker

    很高兴看到 for-of 将进入 ES6。我不知道我遇到过多少次需要重构使用 Array.forEach 的代码,而此时需要跳出循环。

    @Luke,它可能不被“每个浏览器”支持,但添加一个小 polyfill 比为了覆盖范围而加载整个库更具吸引力和效率。

    2015 年 5 月 5 日 下午 18:24

  12. Fedge

    仍然需要使用 Mootools 来处理大多数事情,因为所有新功能的浏览器支持需要几年时间才能赶上(主要是因为人们仍在使用旧版本的 IE)。希望人们使用的框架能够更新,以便在内部利用这些功能,从而提高速度。

    对于可以直接使用该语言而无需担心浏览器差异的情况(例如在 Firefox 扩展程序中),新的 JS 功能将非常棒。

    2015 年 5 月 6 日 下午 14:15

  13. Oleg

    太棒了。FF 似乎支持新的 ES6 语法。Chrome 不支持。尝试在 http://www.squarefree.com/jsenv/ 中复制并执行以下代码。它在 Firefox 36 中有效。

    var myIterableObject = {
    value: 5,

    好的,我知道你在想什么。那个 [Symbol.iterator] 语法看起来很奇怪。这到底是怎么回事?这与方法的名称有关。标准委员会本可以简单地将此方法命名为 .iterator(),但是,你的现有代码中可能已经有一些对象具有 .iterator() 方法,这会造成很大的混乱。因此,标准使用符号而不是字符串作为此方法的名称。
    符号是 ES6 中的新功能,我们会在以后的文章中详细介绍它们。现在,你只需要知道标准可以定义一个全新的符号,比如 Symbol.iterator,并且保证它不会与任何现有代码冲突。代价是语法有点奇怪。但这对于这个多功能的新功能和出色的向后兼容性来说是一个小代价。
    },

    具有 [Symbol.iterator]() 方法的对象称为可迭代对象。在接下来的几周内,我们将看到可迭代对象的概念在整个语言中都有应用,不仅在 forof 中,还包括在 MapSet 构造函数、解构赋值和新的扩展运算符中。
    if (this.value < 0) {
    return {done: true, value: 0};
    }

    var ret = {done: false, value: this.value};
    this.value–;
    return ret;
    }
    };

    for (var v of myIterableObject) {
    print(v);
    }

    2015 年 5 月 7 日 上午 00:00

    1. Jason Orendorff

      抱歉,评论系统会吃掉代码。:-( 我把它复制到这里:http://codepen.io/anon/pen/oXjRPb

      我以为 Chrome 默认启用了它,但没有。你必须进入 chrome://flags 并点击“启用实验性 JavaScript”。感谢你发现这个问题。我会更新文章。

      2015 年 5 月 7 日 上午 08:46

  14. subzey

    看起来“for-in 如何工作”代码中有一个小错误:变量既名为`result` 又名为`$result`。

    2015 年 5 月 13 日 下午 13:49

    1. Jason Orendorff

      哎呀,你说得对。我已经修复了。感谢你指出这个问题!

      2015 年 5 月 13 日 下午 13:57

      1. subzey

        感谢你的帖子!

        2015 年 5 月 13 日 下午 14:03

这篇文章的评论已关闭。