迭代(Iteration)
for..in
循环迭代一个对象上(包括它的 [[Prototype]]
链)所有的可迭代属性。但如果你想要迭代值呢?
在数字索引的数组中,典型的迭代所有的值的办法是使用标准的 for
循环,比如:
var myArray = [1, 2, 3];
for (var i = 0; i < myArray.length; i++) {
console.log( myArray[i] );
}
// 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
循环语法,用来迭代数组(和对象,如果这个对象有定义的迭代器):
var myArray = [ 1, 2, 3 ];
for (var v of myArray) {
console.log( v );
}
// 1
// 2
// 3
for..of
循环要求被迭代的 东西 提供一个迭代器对象(从一个在语言规范中叫做 @@iterator
的默认内部函数那里得到),每次循环都调用一次这个迭代器对象的 next()
方法,循环迭代的内容就是这些连续的返回值。
数组拥有内建的 @@iterator
,所以正如展示的那样,for..of
对于它们很容易使用。但是让我们使用内建的 @@iterator
来手动迭代一个数组,来看看它是怎么工作的:
var myArray = [ 1, 2, 3 ];
var it = myArray[Symbol.iterator]();
it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { done:true }
注意: 我们使用一个 ES6 的 Symbol
:Symbol.iterator
来取得一个对象的 @@iterator
内部属性。我们在本章中简单地提到过 Symbol
的语义(见“计算型属性名”),同样的原理也适用于这里。你总是希望通过 Symbol
名称,而不是它可能持有的特殊的值,来引用这样特殊的属性。另外,尽管这个名称有这样的暗示,但 @@iterator
本身 不是迭代器对象, 而是一个返回迭代器对象的 方法 —— 一个重要的细节!
正如上面的代码段揭示的,迭代器的 next()
调用的返回值是一个 { value: .. , done: .. }
形式的对象,其中 value
是当前迭代的值,而 done
是一个 boolean
,表示是否还有更多内容可以迭代。
注意值 3
和 done:false
一起返回,猛地一看会有些奇怪。你不得不第四次调用 next()
(在前一个代码段的 for..of
循环会自动这样做)来得到 done:true
,以使自己知道迭代已经完成。这个怪异之处的原因超出了我们要在这里讨论的范围,但是它源自于 ES6 生成器(generator)函数的语义。
虽然数组可以在 for..of
循环中自动迭代,但普通的对象 没有内建的 @@iterator
。这种故意省略的原因要比我们将在这里解释的更复杂,但一般来说,为了未来的对象类型,最好不要加入那些可能最终被证明是麻烦的实现。
但是 可以 为你想要迭代的对象定义你自己的默认 @@iterator
。比如:
var myObject = {
a: 2,
b: 3
};
Object.defineProperty( myObject, Symbol.iterator, {
enumerable: false,
writable: false,
configurable: true,
value: function() {
var o = this;
var idx = 0;
var ks = Object.keys( o );
return {
next: function() {
return {
value: o[ks[idx++]],
done: (idx > ks.length)
};
}
};
}
} );
// 手动迭代 `myObject`
var it = myObject[Symbol.iterator]();
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { value:undefined, done:true }
// 用 `for..of` 迭代 `myObject`
for (var v of myObject) {
console.log( v );
}
// 2
// 3
注意: 我们使用了 Object.defineProperty(..)
来自定义我们的 @@iterator
(很大程度上是因为我们可以将它指定为不可枚举的),但是通过将 Symbol
作为一个 计算型属性名(在本章前面的部分讨论过),我们也可以直接声明它,比如 var myObject = { a:2, b:3, [Symbol.iterator]: function(){ /* .. */ } }
。
每次 for..of
循环在 myObject
的迭代器对象上调用 next()
时,迭代器内部的指针将会向前移动并返回对象属性列表的下一个值(关于对象属性/值迭代顺序,参照前面的注意事项)。
我们刚刚演示的迭代,是一个简单的一个值一个值的迭代,当然你可以为你的自定义数据结构定义任意复杂的迭代方法,只要你觉得合适。对于操作用户自定义对象来说,自定义迭代器与 ES6 的 for..of
循环相组合,是一个新的强大的语法工具。
举个例子,一个 Pixel(像素)
对象列表(拥有 x
和 y
的坐标值)可以根据距离原点 (0,0)
的直线距离决定它的迭代顺序,或者过滤掉那些“太远”的点,等等。只要你的迭代器从 next()
调用返回期望的 { value: .. }
返回值,并在迭代结束后返回一个 { done: true }
值,ES6 的 for..of
循环就可以迭代它。
其实,你甚至可以生成一个永远不会“结束”,并且总会返回一个新值(比如随机数,递增值,唯一的识别符等等)的“无穷”迭代器,虽然你可能不会将这样的迭代器用于一个没有边界的 for..of
循环,因为它永远不会结束,而且会阻塞你的程序。
var randoms = {
[Symbol.iterator]: function() {
return {
next: function() {
return { value: Math.random() };
}
};
}
};
var randoms_pool = [];
for (var n of randoms) {
randoms_pool.push( n );
// 不要超过边界!
if (randoms_pool.length === 100) break;
}
这个迭代器会“永远”生成随机数,所以我们小心地仅从中取出 100 个值,以使我们的程序不被阻塞。