迭代与迭代器

定义

  • 官方定义的迭代意思是:按照顺序反复多次执行一段程序,通常会有明确的终止条件。
  • 实际上计数循环就是最简单的迭代。
  • 循环是迭代机制的基础,这是因为它可以指定迭代的次数,以及每次迭代要执行什么操作。每次循环都会在下一次迭代开始之前完成,而每次迭代的顺序都是事先定义好的。
  • 迭代会在一个有序集合上进行。(“有序”可以理解为集合中所有项都可以按照既定的顺序被遍历到,特别是开始和结束项有明确的定义。)数组是JavaScript 中有序集合的最典型例子。

迭代器模式

  • 迭代器模式(特别是在 ECMAScript这个语境下)描述了一个方案,即可以把有些结构称为“可迭代对象”( iterable),因为它们实现了正式的工terable接口,而且可以通过迭代器工terator消费。
  • 基本上,可以把可迭代对象理解成数组或集合这样的集合类型的对

可迭代协议

  • 可迭代协议指的是Iterable接口
  • 实现工terable接口(可迭代协议)要求同时具备两种能力:支持迭代的自我识别能力和创建实现Iterator接口的对象的能力
  • 实现可迭代协议的所有类型都会白动兼容接收可迭代对象的任何语言特性。
  • 接收可迭代对象的原牛语言特性句括:
    • for-of循环
    • 数组解构
    • 扩展操作符
    • Array.from()
    • 创建集合
    • 创建映射
    • Promise.all()
    • Promise.race()
    • yield*操作符,在生成器中使用

迭代器协议

  • 迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。

  • 迭代器API使用next()方法在可迭代对象中遍历数据。

  • 每次成功调用next(),都会返回一个IteratorResult对象,其中包含迭代器返回的下一个值。若不调用next (),则无法知道迭代器的当前位置。

  • next()方法返回的迭代器对象IteratorResult包含两个属性: done和value

    • done是一个布尔值,表示是否还可以再次调用next ()取得下一个值;
    • value包含可迭代对象的下一个值(done为false)、或者undefined ( done为true )。
    • done: true状态称为“耗尽”。
1
2
3
4
5
6
7
8
9
10
11
// 可迭代对象
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 }
  • 不同迭代器的实例相互之间没有联系,只会独立地遍历可迭代对象;
  • 如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化;
1
2
3
4
5
6
7
8
let arr = ['foo', 'baz']; 
let iter = arr[Symbol.iterator]();
console.log(iter.next()); // { done: false, value: 'foo' }
// 在数组中间插入值
arr.splice(1, 0, 'bar');
console.log(iter.next()); // { done: false, value: 'bar' }
console.log(iter.next()); // { done: false, value: 'baz' }
console.log(iter.next()); // { done: true, value: undefined }

自定义迭代器

  • 为了让一个可迭代对象能够创建多个迭代器,必须每创建一个迭代器就对应一个新计数器。为此可以把计数器变量放到闭包里,然后通过闭包返回迭代器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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
for (let i of counter) { console.log(i); }
// 1
// 2
// 3
  • 每个以这种方式创建的迭代器也实现了Iterable接口。Symbol.iterator属性引用的工厂函数会返回相同的迭代器;

提前终止迭代器

  • return()方法用于指定在迭代器提前关闭时执行的逻辑。
  • return()方法必须返回一个有效的工teratorResult对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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 };
}
},
return() {
console.log('Exiting early');
return { done: true };
}
};
}
}
let counter1 = new Counter(5);
for (let i of counter1) {
if (i > 2) {
break;
}
console.log(i);
}
// 1
// 2
// Exiting early
let counter2 = new Counter(5);
try {
for (let i of counter2) {
if (i > 2) {
throw 'err';
}
console.log(i);
}
} catch(e) {}
// 1
// 2
// Exiting early
let counter3 = new Counter(5);
let [a, b] = counter3;
// Exiting early
  • 如果迭代器没有关闭,则还可以继续从上次离开的地方继续迭代。
  • 数组的迭代器就是不能关闭的。
  • 要知道某个迭代器是否可关闭,可以测试这个迭代器实例的return属性是不是函数对象。
  • 调用 return()不会强制迭代器进入关闭状态,但return()方法还是会被调用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let a = [1, 2, 3, 4, 5]; 
let iter = a[Symbol.iterator]();
iter.return = function() {
console.log('Exiting early');
return { done: true };
};
for (let i of iter) {
console.log(i);
if (i > 2) {
break
}
}
// 1
// 2
// 3
// 提前退出
for (let i of iter) {
console.log(i);
}
// 4
// 5

生成器

  • 生成器是 ECMAScript 6新增的一个极为灵活的结构,拥有在一个函数块内暂停和恢复代码执行的能力。
  • 生成器的形式是一个函数,函数名称前面加一个星号(*)表示它是一个生成器。
  • 只要是可以定义丽数的地方,就可以定义生成器
  • 箭头函数不能用来定义生成器函数。
  • 标识生成器函数的星号不受两侧空格的影响。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 生成器函数声明
function* generatorFn() {}
// 生成器函数表达式
let generatorFn = function* () {}
// 作为对象字面量方法的生成器函数
let foo = {
* generatorFn() {}
}
// 作为类实例方法的生成器函数
class Foo {
* generatorFn() {}
}
// 作为类静态方法的生成器函数
class Bar {
static * generatorFn() {}
}
  • 调用生成器函数会产生一个生成器对象。
  • 生成器对象一开始处于暂停执行( suspended )的状态。
  • 与迭代器相似,生成器对象也实现了Iterator 接口,因此具有next()方法。
  • 调用这个方法会让生成器开始或恢复执行。
1
2
3
4
function* generatorFn() {} 
const g = generatorFn();
console.log(g); // generatorFn {<suspended>}
console.log(g.next); // f next() { [native code] }
  • next ()方法的返回值类似于迭代器,有一个done属性和一个value属性。
  • 函数体为空的生成器函数中间不会停留,调用一次next()就会让生成器到达 done: true状态。
  • value属性是生成器函数的返回值,默认值为undefined,可以通过生成器函数的返回值指定:
1
2
3
4
5
6
function* generatorFn() { 
return 'foo';
}
let generatorObject = generatorFn();
console.log(generatorObject); // generatorFn {<suspended>}
console.log(generatorObject.next()); // { done: true, value: 'foo' }
  • 生成器函数只会在初次调用next ()方法后开始执行。
1
2
3
4
5
6
function* generatorFn() { 
console.log('foobar');
}
// 初次调用生成器函数并不会打印日志
let generatorObject = generatorFn();
generatorObject.next(); // foobar
  • 生成器对象实现了Iterable接口,它们默认的迭代器是自引用的。
1
2
3
4
5
6
7
8
9
10
11
12
function* generatorFn() {} 
console.log(generatorFn);
// f* generatorFn() {}
console.log(generatorFn()[Symbol.iterator]);
// f [Symbol.iterator]() {native code}
console.log(generatorFn());
// generatorFn {<suspended>}
console.log(generatorFn()[Symbol.iterator]());
// generatorFn {<suspended>}
const g = generatorFn();
console.log(g === g[Symbol.iterator]());
// true

通过yield中断执行

  • yield关键字可以让生成器停止和开始执行。
  • 生成器函数在遇到yield关键字之前会正常执行。遇到这个关键字后,执行会停止,函数作用域的状态会被保留。
  • 停止执行的生成器函数只能通过在生成器对象上调用next ()方法来恢复执行。
  • yield关键字生成的值会出现在next()方法返回的对象里
  • 通过yield关键字退出的生成器函数会处在done: false状态。
  • 通过return 关键字退出的生成器函数会处于done: true状态。
1
2
3
4
5
6
7
8
9
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 ()不会影响其他生成器
  • yield关键字只能在生成器函数内部使用,用在其他地方会抛出错误。
  • yield关键字必须直接位于生成器函数定义中,出现在嵌套的非生成器函数中会抛出语法错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 有效
function* validGeneratorFn() {
yield;
}
// 无效
function* invalidGeneratorFnA() {
function a() {
yield;
}
}
// 无效
function* invalidGeneratorFnB() {
const b = () => {
yield;
}
}
// 无效
function* invalidGeneratorFnC() {
(() => {
yield;
})();
}

1、生成器对象作为可迭代对象

  • 把生成器对象当成可迭代对象,那么使用起来会更方便。
1
2
3
4
5
6
7
8
9
10
11
function* generatorFn() { 
yield 1;
yield 2;
yield 3;
}
for (const x of generatorFn()) {
console.log(x);
}
// 1
// 2
// 3
1
2
3
4
5
6
7
8
9
10
11
function* nTimes(n) { 
while(n--) {
yield;
}
}
for (let _ of nTimes(3)) {
console.log('foo');
}
// foo
// foo
// foo

使用yield实现输入和输出

  • 除了可以作为函数的中间返回语句使用, yield关键字还可以作为函数的中间参数使用;
  • 第一次调用next ()传人的值不会被使用,因为这一次调用是为了开始执行生成器函数:
1
2
3
4
5
6
7
8
9
function* generatorFn(initial) { 
// console.log(initial);
console.log(yield);
console.log(yield);
}
let generatorObject = generatorFn('foo');
generatorObject.next('bar'); // 没有输出,开始执行生成器函数
generatorObject.next('baz'); // baz
generatorObject.next('qux'); // qux
  • yield关键字可以同时用于输入和输出:
1
2
3
4
5
6
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' }
  • 因为函数必须对整个表达式求值才能确定要返回的值,所以它在遇到yield关键字时暂停执行并计算出要产生的值: “foo”。下一次调用next ()传入了”bar”,作为交给同一个yield 的值。然后这个值被确定为本次生成器函数要返回的值。
  • yield 关键字并非只能使用一次,如下面的例子定义了一个无穷计数生成器函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
function* generatorFn() { 
for (let i = 0;;++i) {
yield i;
}
}
let generatorObject = generatorFn();
console.log(generatorObject.next().value); // 0
console.log(generatorObject.next().value); // 1
console.log(generatorObject.next().value); // 2
console.log(generatorObject.next().value); // 3
console.log(generatorObject.next().value); // 4
console.log(generatorObject.next().value); // 5
...
  • 使用生成器也可以实现范围和填充数组:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function* range(start, end) { 
while(end > start) {
yield start++;
}
}
for (const x of range(4, 7)) {
console.log(x);
}
// 4
// 5
// 6
function* zeroes(n) {
while(n--) {
yield 0;
}
}
console.log(Array.from(zeroes(8))); // [0, 0, 0, 0, 0, 0, 0, 0]

产生可迭代对象

  • yield星号两侧的空格不影响其行为;
  • 使用星号增强yield的行为,让它能够迭代一个可迭代对象,从而一次产出一个值:
1
2
3
4
5
6
7
8
9
10
function* generatorFn() { 
yield* [1, 2, 3];
}
let generatorObject = generatorFn();
for (const x of generatorFn()) {
console.log(x);
}
// 1
// 2
// 3
  • yield*实际上只是将一个可迭代对象序列化为一连串可以单独产出的值,所以这跟把yield放到一个循环里没什么不同。
  • yield*的值是关联迭代器返回done: true 时的 value属性。对于普通迭代器来说,这个值是undefined。
1
2
3
4
5
6
7
8
9
10
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
  • 对于生成器函数产生的迭代器来说,这个值就是生成器函数返回的值:
1
2
3
4
5
6
7
8
9
10
11
12
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

使用yield*实现递归算法

1
2
3
4
5
6
7
8
9
10
11
12
function* nTimes(n) { 
if (n > 0) {
yield* nTimes(n - 1);
yield n - 1;
}
}
for (const x of nTimes(3)) {
console.log(x);
}
// 0
// 1
// 2

生成器作为默认迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

提前终止生成器

return()

  • return()方法会强制生成器进人关闭状态。提供给return()方法的值,就是终止迭代器对象的值
1
2
3
4
5
6
7
8
9
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状态,而提供的任何返回值都不会被存储或传播:
1
2
3
4
5
6
7
8
9
10
11
function* generatorFn() { 
for (const x of [1, 2, 3]) {
yield x;
}
}
const g = generatorFn();
console.log(g.next()); // { done: false, value: 1 }
console.log(g.return(4)); // { done: true, value: 4 }
console.log(g.next()); // { done: true, value: undefined }
console.log(g.next()); // { done: true, value: undefined }
console.log(g.next()); // { done: true, value: undefined }

throw()

  • throw()方法会在暂停的时候将一个提供的错误注人到生成器对象中。如果错误未被处理,生成器就会关闭。
  • 假如生成器函数内部处理了这个错误,那么生成器就不会关闭,而且还可以恢复执行。
  • 错误处理会跳过对应的yield。
1
2
3
4
5
6
7
8
9
10
11
function* generatorFn() { 
for (const x of [1, 2, 3]) {
try {
yield x;
} catch(e) {}
}
}
const g = generatorFn();
console.log(g.next()); // { done: false, value: 1}
g.throw('foo');
console.log(g.next()); // { done: false, value: 3}

230