迭代(Iteration)

for..in 循环迭代一个对象上(包括它的 [[Prototype]] 链)所有的可迭代属性。但如果你想要迭代值呢?

在数字索引的数组中,典型的迭代所有的值的办法是使用标准的 for 循环,比如:

  1. var myArray = [1, 2, 3];
  2. for (var i = 0; i < myArray.length; i++) {
  3. console.log( myArray[i] );
  4. }
  5. // 1 2 3

但是这并没有迭代所有的值,而是迭代了所有的下标,然后由你使用索引来引用值,比如 myArray[i]

ES5 还为数组加入了几个迭代帮助方法,包括 forEach(..)every(..)、和 some(..)。这些帮助方法的每一个都接收一个回调函数,这个函数将施用于数组中的每一个元素,仅在如何响应回调的返回值上有所不同。

forEach(..) 将会迭代数组中所有的值,并且忽略回调的返回值。every(..) 会一直迭代到最后,或者 当回调返回一个 false(或“falsy”)值,而 some(..) 会一直迭代到最后,或者 当回调返回一个 true(或“truthy”)值。

这些在 every(..)some(..) 内部的特殊返回值有些像普通 for 循环中的 break 语句,它们可以在迭代执行到末尾之前将它结束掉。

如果你使用 for..in 循环在一个对象上进行迭代,你也只能间接地得到值,因为它实际上仅仅迭代对象的所有可枚举属性,让你自己手动地去访问属性来得到值。

注意: 与以有序数字的方式(for 循环或其他迭代器)迭代数组的下标比较起来,迭代对象属性的顺序是 不确定 的,而且可能会因 JS 引擎的不同而不同。对于需要跨平台环境保持一致的问题,不要依赖 观察到的顺序,因为这个顺序是不可靠的。

但是如果你想直接迭代值,而不是数组下标(或对象属性)呢?ES6 加入了一个有用的 for..of 循环语法,用来迭代数组(和对象,如果这个对象有定义的迭代器):

  1. var myArray = [ 1, 2, 3 ];
  2. for (var v of myArray) {
  3. console.log( v );
  4. }
  5. // 1
  6. // 2
  7. // 3

for..of 循环要求被迭代的 东西 提供一个迭代器对象(从一个在语言规范中叫做 @@iterator 的默认内部函数那里得到),每次循环都调用一次这个迭代器对象的 next() 方法,循环迭代的内容就是这些连续的返回值。

数组拥有内建的 @@iterator,所以正如展示的那样,for..of 对于它们很容易使用。但是让我们使用内建的 @@iterator 来手动迭代一个数组,来看看它是怎么工作的:

  1. var myArray = [ 1, 2, 3 ];
  2. var it = myArray[Symbol.iterator]();
  3. it.next(); // { value:1, done:false }
  4. it.next(); // { value:2, done:false }
  5. it.next(); // { value:3, done:false }
  6. it.next(); // { done:true }

注意: 我们使用一个 ES6 的 SymbolSymbol.iterator 来取得一个对象的 @@iterator 内部属性。我们在本章中简单地提到过 Symbol 的语义(见“计算型属性名”),同样的原理也适用于这里。你总是希望通过 Symbol 名称,而不是它可能持有的特殊的值,来引用这样特殊的属性。另外,尽管这个名称有这样的暗示,但 @@iterator 本身 不是迭代器对象, 而是一个返回迭代器对象的 方法 —— 一个重要的细节!

正如上面的代码段揭示的,迭代器的 next() 调用的返回值是一个 { value: .. , done: .. } 形式的对象,其中 value 是当前迭代的值,而 done 是一个 boolean,表示是否还有更多内容可以迭代。

注意值 3done:false 一起返回,猛地一看会有些奇怪。你不得不第四次调用 next()(在前一个代码段的 for..of 循环会自动这样做)来得到 done:true,以使自己知道迭代已经完成。这个怪异之处的原因超出了我们要在这里讨论的范围,但是它源自于 ES6 生成器(generator)函数的语义。

虽然数组可以在 for..of 循环中自动迭代,但普通的对象 没有内建的 @@iterator。这种故意省略的原因要比我们将在这里解释的更复杂,但一般来说,为了未来的对象类型,最好不要加入那些可能最终被证明是麻烦的实现。

但是 可以 为你想要迭代的对象定义你自己的默认 @@iterator。比如:

  1. var myObject = {
  2. a: 2,
  3. b: 3
  4. };
  5. Object.defineProperty( myObject, Symbol.iterator, {
  6. enumerable: false,
  7. writable: false,
  8. configurable: true,
  9. value: function() {
  10. var o = this;
  11. var idx = 0;
  12. var ks = Object.keys( o );
  13. return {
  14. next: function() {
  15. return {
  16. value: o[ks[idx++]],
  17. done: (idx > ks.length)
  18. };
  19. }
  20. };
  21. }
  22. } );
  23. // 手动迭代 `myObject`
  24. var it = myObject[Symbol.iterator]();
  25. it.next(); // { value:2, done:false }
  26. it.next(); // { value:3, done:false }
  27. it.next(); // { value:undefined, done:true }
  28. // 用 `for..of` 迭代 `myObject`
  29. for (var v of myObject) {
  30. console.log( v );
  31. }
  32. // 2
  33. // 3

注意: 我们使用了 Object.defineProperty(..) 来自定义我们的 @@iterator(很大程度上是因为我们可以将它指定为不可枚举的),但是通过将 Symbol 作为一个 计算型属性名(在本章前面的部分讨论过),我们也可以直接声明它,比如 var myObject = { a:2, b:3, [Symbol.iterator]: function(){ /* .. */ } }

每次 for..of 循环在 myObject 的迭代器对象上调用 next() 时,迭代器内部的指针将会向前移动并返回对象属性列表的下一个值(关于对象属性/值迭代顺序,参照前面的注意事项)。

我们刚刚演示的迭代,是一个简单的一个值一个值的迭代,当然你可以为你的自定义数据结构定义任意复杂的迭代方法,只要你觉得合适。对于操作用户自定义对象来说,自定义迭代器与 ES6 的 for..of 循环相组合,是一个新的强大的语法工具。

举个例子,一个 Pixel(像素) 对象列表(拥有 xy 的坐标值)可以根据距离原点 (0,0) 的直线距离决定它的迭代顺序,或者过滤掉那些“太远”的点,等等。只要你的迭代器从 next() 调用返回期望的 { value: .. } 返回值,并在迭代结束后返回一个 { done: true } 值,ES6 的 for..of 循环就可以迭代它。

其实,你甚至可以生成一个永远不会“结束”,并且总会返回一个新值(比如随机数,递增值,唯一的识别符等等)的“无穷”迭代器,虽然你可能不会将这样的迭代器用于一个没有边界的 for..of 循环,因为它永远不会结束,而且会阻塞你的程序。

  1. var randoms = {
  2. [Symbol.iterator]: function() {
  3. return {
  4. next: function() {
  5. return { value: Math.random() };
  6. }
  7. };
  8. }
  9. };
  10. var randoms_pool = [];
  11. for (var n of randoms) {
  12. randoms_pool.push( n );
  13. // 不要超过边界!
  14. if (randoms_pool.length === 100) break;
  15. }

这个迭代器会“永远”生成随机数,所以我们小心地仅从中取出 100 个值,以使我们的程序不被阻塞。