1 理解迭代
循环是迭代机制的基础,这是因为它可以指定迭代的次数,以及每次迭代要执行什么操作。每次循
环都会在下一次迭代开始之前完成,而每次迭代的顺序都是事先定义好的。
let collection = ['foo', 'bar', 'baz'];
for (let index = 0; index < collection.length; ++index) {
console.log(collection[index]);
}
通过这种循环来执行例程并不理想,因为:
- 迭代之前需要事先知道如何使用数据结构。数组中的每一项都只能先通过引用取得数组对象,
然后再通过[]操作符取得特定索引位置上的项。这种情况并不适用于所有数据结构。 - 遍历顺序并不是数据结构固有的。通过递增索引来访问数据是特定于数组类型的方式,并不适 用于其他具有隐式顺序的数据结构。
ES5 新增 Array.prototype.forEach()方法,解决了单独记录索引和通过数组对象取得值的问题。不过,没有办法标识迭代何时终止。因此这个方法只适用于数组。解决方案ES6支持的迭代器模式
2 迭代器模式
迭代器模式:实现了正式的 Iterable 接口,可以把可迭代对象理解成数组或集合这样的集合类型的对
象。它们包含的元素都是有限的,而且都具有无歧义的遍历顺序。
// 数组的元素是有限的
// 递增索引可以按序访问每个元素
let arr = [3, 1, 4];
// 集合的元素是有限的
// 可以按插入顺序访问每个元素
let set = new Set().add(3).add(1).add(4);
可迭代对象不一定是集合对象,也可以是仅仅具有类似数组行为的其他数据结构,比如计数循环。该循环中生成的值是暂时性的,但循环本身是在执行迭代。
for (let i = 1; i <= 10; ++i) {
console.log(i);
}
2.1 可迭代协议
这个默认迭代器属性必须引用一个迭代器工厂
函数,Symbol.iterator作为键,很多内置类型都实现了 Iterable 接口:
- 字符串数组
- 数组
- 映射
- 集合
- arguments 对象
- NodeList 等 DOM 集合类型
检查是否存在默认迭代器属性可以暴露这个工厂函数:
let obj = {};
console.log(obj[Symbol.iterator]); // undefined
接收可迭代对象的原生语言特性包括:
for-of 循环;
数组解构 : let [a, b, c] = arr;
扩展操作符:let arr2 = […arr];
Array.from()
创建集合:new Set(arr);
创建映射
Promise.all()接收由期约组成的可迭代对象
Promise.race()接收由期约组成的可迭代对象
yield*操作符,在生成器中使用
2.2 迭代器协议
迭代器 API 使用 next()方法在可迭代对象中遍历数据。每次成功调用 next(),都会返回一个 IteratorResult 对象包含两个属性:done 和 value。
// 可迭代对象
let arr = ['foo', 'bar'];
// 迭代器工厂函数
console.log(arr[Symbol.iterator]); // f values() { [native code] }
// 迭代器
let iter = arr[Symbol.iterator]();
console.log(iter); // ArrayIterator {}
// 执行迭代
console.log(iter.next()); // { done: false, value: 'foo' }
console.log(iter.next()); // { done: false, value: 'bar' }
console.log(iter.next()); // { done: true, value: undefined }
注意 迭代器维护着一个指向可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可
迭代对象。
2.3 自定义迭代器
让一个可迭代对象能够创建多个迭代器,必须每创建一个迭代器就对应一个新计数器。为此,
可以把计数器变量放到闭包里,然后通过闭包返回迭代器
class Counter {
constructor(limit) {
this.limit = limit;
}
[Symbol.iterator]() {
let count = 1,
limit = this.limit;
return {
next() {
if (count <= limit) {
return { done: false, value: count++ };
} else {
return { done: true, value: undefined };
}
}
};
}
}
let counter = new Counter(3);
for (let i of counter) { console.log(i); }
// 1
// 2
// 3
2.4 提前终止迭代器
可选的 return()方法用于指定在迭代器提前关闭时执行的逻辑。for-of 循环通过 break、continue、return 或 throw 提前退出。
3 生成器
生成器是 ECMAScript 6 新增的一个极为灵活的结构,拥有在一个函数块内暂停和恢复代码执行的
能力。
3.1 生成器基础
生成器的形式是一个函数,函数名称前面加一个星号(*)表示它是一个生成器。只要是可以定义
函数的地方,就可以定义生成器。
// 生成器函数声明
function* generatorFn() {}
// 生成器函数表达式
let generatorFn = function* () {}
// 作为对象字面量方法的生成器函数
let foo = {
* generatorFn() {}
}
// 作为类实例方法的生成器函数
class Foo {
* generatorFn() {}
}
// 作为类静态方法的生成器函数
class Bar {
static * generatorFn() {}
}
注意 箭头函数不能用来定义生成器函数。
生成器对象一开始处于暂停执行(suspended)的状态。生成器对象也实现了 Iterator 接口,因此具有 next()方法。调用这个方法会让生成器开始或恢复执行。
function* generatorFn() {}
const g = generatorFn();
console.log(g); // generatorFn {<suspended>}
console.log(g.next); // f next() { [native code] }
如上,因为函数体为空的生成器函数中间不会停留,调用一次 next()就会让生成器到达 done: true 状态。
console.log(g.next()); // { done: true, value: undefined }
value 属性是生成器函数的返回值,默认值为 undefined,可以通过生成器函数的返回值指定:
function* generatorFn() {
return 'foo';
}
let generatorObject = generatorFn();
console.log(generatorObject.next()); // { done: true, value: 'foo' }
3.2 通过yield中断执行
yield 关键字可以让生成器停止,next()方法恢复执行:
function* generatorFn() {
yield;
}
let generatorObject = generatorFn();
console.log(generatorObject.next()); // { done: false, value: undefined }
console.log(generatorObject.next()); // { done: true, value: undefined }
yield生成的值会出现在 next()方法返回的对象里。yield 关键字退出生成器函数会处在 done: false 状态;通过 return 关键字退出的生成器函数会处于 done: true 状态。
function* generatorFn() {
yield 'foo';
yield 'bar';
return 'baz';
}
let generatorObject = generatorFn();
console.log(generatorObject.next()); // { done: false, value: 'foo' }
console.log(generatorObject.next()); // { done: false, value: 'bar' }
console.log(generatorObject.next()); // { done: true, value: 'baz' }
生成器函数内部的执行流程会针对每个生成器对象区分作用域。在一个生成器对象上调用 next()
不会影响其他生成器:
function* generatorFn() {
yield 'foo';
yield 'bar';
return 'baz';
}
let generatorObject1 = generatorFn();
let generatorObject2 = generatorFn();
console.log(generatorObject1.next()); // { done: false, value: 'foo' }
console.log(generatorObject2.next()); // { done: false, value: 'foo' }
yield 关键字只能在生成器函数内部使用,用在其他地方会抛出错误。类似函数的 return 关键字:
// 有效
function* validGeneratorFn() {
yield;
}
// 无效
function* invalidGeneratorFnA() {
function a() {
yield;
}
}
1.生成器对象作为可迭代对象
把生成器对象当成可迭代对象:
function* generatorFn() {
yield 1;
yield 2;
yield 3;
}
for (const x of generatorFn()) {
console.log(x);
}
// 1
// 2
// 3
在需要自定义迭代对象时,这样使用生成器对象会特别有用:
function* nTimes(n) {
while(n--) {
yield;
}
}
for (let _ of nTimes(3)) {
console.log('foo');
}
// foo
// foo
// foo
2.使用 yield 实现输入和输出
yield 关键字还可以作为函数的中间参数使用,让生成器函数暂停的 yield 关键字会接收到传给 下个next()方法的值。第一次调用 next()传入的值不会被使用,因为这一次调用是为了开始执行生成器函数:
function* generatorFn(initial) {
console.log(initial);
console.log(yield);
console.log(yield);
}
let generatorObject = generatorFn('foo');
generatorObject.next('bar'); // foo
generatorObject.next('baz'); // baz
generatorObject.next('qux'); // qux
yield 关键字可以同时用于输入和输出,如下例所示:
function* generatorFn() {
return yield 'foo';
}
let generatorObject = generatorFn();
console.log(generatorObject.next()); // { done: false, value: 'foo' }
console.log(generatorObject.next('bar')); // { done: true, value: 'bar' }
因为函数必须对整个表达式求值才能确定要返回的值,return yield 暂停执行并计算要产生的值:“foo”,下一次调用 next()传入了"bar",作为交给同一个 yield 的值。然后这个值被确定为本次生成器函数要返回的值。
3.产生可迭代对象
使用星号增强 yield 的行为,让它能够迭代一个可迭代对象,从而一次产出一个值:
// 等价的 generatorFn:
// function* generatorFn() {
// for (const x of [1, 2, 3]) {
// yield x;
// }
// }
function* generatorFn() {
yield* [1, 2, 3];
}
let generatorObject = generatorFn();
for (const x of generatorFn()) {
console.log(x);
}
// 1
// 2
// 3
yield*的值是关联迭代器返回 done: true 时的 value 属性。对于普通迭代器来说,这个值是undefined:
function* generatorFn() {
console.log('iter value:', yield* [1, 2, 3]);
}
for (const x of generatorFn()) {
console.log('value:', x);
}
// value: 1
// value: 2
// value: 3
// iter value: undefined
对于生成器函数产生的迭代器来说,这个值就是生成器函数返回的值:
function* innerGeneratorFn() {
yield 'foo';
return 'bar';
}
function* outerGeneratorFn(genObj) {
console.log('iter value:', yield* innerGeneratorFn());
}
for (const x of outerGeneratorFn()) {
console.log('value:', x);
}
// value: foo
// iter value: bar
4.使用yield*实现递归算法
yield*最有用的地方是实现递归操作
3.3 生成器作为默认迭代器
class Foo {
constructor() {
this.values = [1, 2, 3];
}
* [Symbol.iterator]() {
yield* this.values;
}
}
const f = new Foo();
for (const x of f) {
console.log(x);
}
// 1
// 2
// 3
这里,for-of 循环调用了默认迭代器(它恰好又是一个生成器函数)并产生了一个生成器对象。
这个生成器对象是可迭代的,所以完全可以在迭代中使用。
3.4 提前终止生成器
与迭代器类似,生成器也支持“可关闭”的概念。一个实现 Iterator 接口的对象一定有 next()方法,还有一个可选的 return()方法用于提前终止迭代器。生成器对象除了有这两个方法,还有第三个方法:throw()。
function* generatorFn() {
for (const x of [1, 2, 3]) {
yield x;
}
}
const g = generatorFn();
console.log(g); // generatorFn {<suspended>}
console.log(g.return(4)); // { done: true, value: 4 }
console.log(g); // generatorFn {<closed>}
与迭代器不同,所有生成器对象都有 return()方法,只要通过它进入关闭状态,就无法恢复了。
后续调用 next()会显示 done: true 状态
throw()方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器就会关闭,处理了这个错误,那么生成器就不会关闭,而且还可以恢复执行。错误处理会跳过对应的 yield。
4 小结
ECMAScript 6 正式支持迭代模式并引入了两个新的语言特性:迭代器和生成器。
迭代器是一个可以由任意对象实现的接口,支持连续获取对象产出的每一个值。任何实现 Iterable接口的对象都有一个 Symbol.iterator 属性,这个属性引用默认迭代器。默认迭代器就像一个迭代器工厂,也就是一个函数,调用之后会产生一个实现 Iterator 接口的对象。
迭代器必须通过连续调用 next()方法才能连续取得值,这个方法返回一个 IteratorObject。这个对象包含一个 done 属性和一个 value 属性。前者是一个布尔值,表示是否还有更多值可以访问;后者包含迭代器返回的当前值。这个接口可以通过手动反复调用 next()方法来消费,也可以通过原生消费者,比如 for-of 循环来自动消费。
生成器是一种特殊的函数,调用之后会返回一个生成器对象。生成器对象实现了 Iterable 接口,因此可用在任何消费可迭代对象的地方。生成器的独特之处在于支持 yield 关键字,这个关键字能够暂停执行生成器函数。使用 yield 关键字还可以通过 next()方法接收输入和产生输出。在加上星号之后,yield 关键字可以将跟在它后面的可迭代对象序列化为一连串值。