迭代器(Iterators)和生成器(Generator)

迭代器(Iterators)和生成器(Generator)

迭代器

迭代器(iterator),使用户可在容器对象(container,例如链表数组)上遍历的对象

迭代器模式

迭代器模式可以让开发者无需了解如何迭代就可以实现迭代操作。Python,C#,Java对该模式都有完备的支持,JavaScript在ECMAScript 6 也将其引入到语言核心中来。

迭代器是一个可以由任意对象实现的接口,支持连续获取对象产出的每一个值。任何实现Iterable接口的对象都有一个 Symbol.iterator 属性,该属性引用默认迭代器。默认迭代器就是一个工厂函数,调用后会产生一个实现 Iterator 接口的对象。若一个对象拥有迭代行为该对象就是一个可迭代对象

可迭代协议(Iterable )和迭代器协议(Iterator)

实现Iterable接口要求同时具备两种能力:

  • 支持迭代自我识别能力

    Symbol.iterator 键(key)的属性,暴露该属性作为”默认迭代器“,并引用一个迭代器工厂函数

  • 创建Iterator 接口的对象的能力

    一个迭代器工厂函数,调用这个工厂函数必须返回一个迭代器

迭代器协议(Iterator)是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器API 使用next() 方法再可迭代对象中遍历数据。

实现了Iterable接口的内置类型

  • 字符串

  • 数组

  • 映射

  • 集合

  • arguments对象

  • NodeListDOM集合类型

如代码所示:

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
let num = 1;
let obj = {};

// 没有实现了迭代器工厂函数的类型
console.log(num[Symbol.iterator]); // undefined
console.log(obj[Symbol.iterator]); // undefined

let str = 'banana';;
let arr = [1, 2, 3];
let map = new Map();
let set = new Set();
let element = document.querySelectorAll('div');

// 实现了迭代器工厂函数的类型
console.log(str[Symbol.iterator]); // ƒ [Symbol.iterator]()
console.log(arr[Symbol.iterator]); // ƒ values()
console.log(map[Symbol.iterator]); // ƒ entries()
console.log(set[Symbol.iterator]); // ƒ values()
console.log(element[Symbol.iterator]);// ƒ values()

// 调用工厂函数就生成一个迭代器
let strIterator = str[Symbol.iterator]();
let arrIterator = arr[Symbol.iterator]();
let mapIterator = map[Symbol.iterator]();
let setIterator = set[Symbol.iterator]();
let elementIterator = element[Symbol.iterator]();
console.log(strIterator); // StringIterator
console.log(arrIterator); // Array Iterator
console.log(mapIterator); // MapIterator
console.log(setIterator); //SetIterator
console.log(elementIterator); //Array Iterator

可迭代对象的语法

对于可接受迭代对象的原生语言特性而言,实际编码过程中不需要显式调用这个工厂函数来生成迭代器,这些原生语言结构会在后台调用提供的可迭代对象的这个工厂函数,从而创建一个迭代器。

支持可迭代对象的原生语言结构包括:

  • for-of 循环

  • 数组结构

  • 扩展操作符

  • Array.from()

  • 创建集合

  • 创建映射

  • Promise.all()Promise.race 接收可由Promise组成的可迭代对象

  • yield* 操作符,在生成器中使用

如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let arr = ['a', 'b', 'c'];
for (const el of arr) {
console.log(el);
}

let [a, b, c] = arr;
console.log(a, b, c); // a b c

let arr2 = [...arr];
console.log(arr2); // clone of arr

let arr3 = Array.from(arr);
console.log(arr3); // clone of arr

let set = new Set(arr);
console.log(set); // Set(3) {size: 3, a, b, c}

let pairs = arr.map((el, i) => [i, el]);
let map = new Map(pairs);
console.log(pairs); // [Array(2), Array(2), Array(2)]
console.log(map); // Map(3) {size: 3, 0 => a, 1 => b, 2 => c}

自定义迭代器

Iterable接口类似,任何实现了Iterator接口的对象都可以作为迭代器使用,Symbol.iterator 属性引用的工厂函数会返回相同的迭代器。

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
class Counter {
constructor(limit = 0) {
this.count = 0;
this.limit = limit;
}
[Symbol.iterator]() {
// 为了让一个迭代对象开源创建多个迭代器(比如重新执行for...of),必须每次创建一个迭代器就对应一个新的计数器
// 所以需要把计数器变量放到闭包中
let count = this.count;
let limit = this.limit;
return {
next() {
if (count < limit) {
return { value: ++count, done: false };
} else {
return { done: true };
}
}
}
}
}

let counter = new Counter(4);
for (let c of counter) {
console.log(c);
if (c > 2) {
break;
}
}
//1,2

console.log(counter[Symbol.iterator]()); // {next: ƒ}
let counter2 = new Counter(5);
console.log(counter1[Symbol.iterator] === counter2[Symbol.iterator]); //true

提前终止迭代器

可选的 return() 方法(只能返回: {done:true})用户指定在迭代器提前关闭时执行的逻辑。而执行迭代的结构想让迭代器提前关闭,可能包括的情况有:

  • for-of 循环通过 breakcontinuereturnthrow提前退出

  • 结构操作并未消费所有的值

如下所示:

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
class Counter {
constructor(limit = 0) {
this.limit = limit;
}

[Symbol.iterator]() {
let count = 1, limit = this.limit;
return {
next() {
if (count <= limit) {
return { value: count++, done: false };
} else {
return { done: true };
}
},
return() {
console.log('Exitng early');
return { done: true };
}
};
}
}

let counter1 = new Counter(5);
for (let i of counter1) {
if (i > 2) {
break;
}
console.log(i);
}
// 1,2 提前退出

let counter2 = new Counter(5);
try {
for (let i of counter2) {
if (i > 2) {
throw new Error('Error');
}
console.log(i);
}
} catch (e) { }
// 1,2 提前退出

let counter3 = new Counter(5);
let [a, b] = counter3;
// 1,2 提前退出

如果迭代器没有关闭,那么还可以继续从上次离开的地方继续迭代。数组的迭代器就是不能关闭的:

1
2
3
4
5
6
7
8
9
10
11
12
13
let arr = [1, 2, 3, 4, 5];
let iter = arr[Symbol.iterator]();

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

生成器函数

生成器是一种特殊的函数。最初调用时,生成器函数不执行任何代码,调用之后会返回一个生成器对象(Generator)。生成器对象实现了Iterable 接口,所以可以用于任何消费可迭代对象的地方。生成器的独特之处在于支持 yield 关键字,该关键字能够暂停执行函数。使用yield关键字还可以通过next() 方法接受输入和产出的值。再加上*号之后,yield 关键字可以将跟在它后面的可迭代对象序列化为一连串的值。

📌生成器函数是一种特殊类型的函数。标准类型的函数每次执行的时候都会创建一个新的环境上下文,而生成器的执行环境上下文会暂时挂起并在将来恢复。

通过迭代器对象控制生成器

迭代器用于控制生成器的执行。迭代器对象暴露的最基本接口是 next() 方法。该方法开源用来向生成器请求一个值,从而控制生成器。

下面的代码定义了一个生成器函数,生成器函数只会在初次调用next() 方法后开始执行,调用该函数会返回一个生成器对象。该对象一开始处于暂停执行的状态。调用next() 方法会让生成器开始或恢复执行,碰到 yield时会返回,并重新进入暂停状态并且当前函数作用域的状态会被保留。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* generatorNum() {
yield 1;
yield 2;
}

const gen = generatorNum();
console.log(gen); // generatorNum {[[GeneratorState]]: 'suspended'}
console.log(gen.next()); // {value: 1, done: false}
console.log(gen.next()); // {value: 2, done: false}
console.log(gen);// generatorNum {[[GeneratorState]]: 'suspended'}
console.log(gen.next()); // {value: undefined, done: true}
console.log(gen); // generatorNum {[[GeneratorState]]: 'closed'}

// 生成器对象实现了 Iterable 接口,默认的迭代器是自引用的
console.log(gen[Symbol.iterator]() === gen); // true

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

因为生成器对象也实现了 Iterator 接口,所以可以直接使用可迭代对象的语法:

1
2
3
4
5
6
7
8
function* generatorNum() {
yield 1;
yield 2;
}

for (let i of generatorNum()) {
console.log(i);
}

把执行权交给下一个生成器对象

yield* 语句可以将当前生成器对象的执行权交给另一个生成器对象。或者说yield* 语句可以迭代一个可迭代的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function* generatorNum() {
yield 1;
yield 2;
// 返回值为 c,如果没有返回值则为 undefined
console.log(yield* generatorStr())
yield* [3, 4];
}

function* generatorStr() {
yield 'a';
yield 'b';
return 'c';
}

for (let i of generatorNum()) {
console.log(i);
}
//1,2,a,b,c,3,4

使用生成器

使用生成器生成ID序列

下面的例子中记录ID的变量无法在生成器外部被修改,idGenerator 迭代器可以一直向生成器请求新的ID值。生成器会按需计算它们的产生值,这使得它们能够有效的表示一个计算成本很高的序列,如下面的例子所示,如果是普通函数,是不推荐使用 while(true) 循环的,但是生成器函数却不会有问题。

1
2
3
4
5
6
7
8
9
10
11
12
function* IdGenerator() {
let id = 0;
while (true) {
yield ++id;
}
}

const idGenerator = IdGenerator();
const id1 = idGenerator.next().value;
const id2 = idGenerator.next().value;
const id3 = idGenerator.next().value;
console.log(id1, id2, id3);// 1,2,3

使用 yield* 实现递归算法

通过生成器的方式实现的递归我们可以不必依赖回调函数,直接在 for of 中就可以对元素进行操作。同时,使用生成器函数来解耦代码,将生产值的代码(dom节点)和消费值(对dom节点的操作)的代码隔离开

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
/**
* 生成器递归函数
* @param {*} elelemt
*/
function* DomTaversal(elelemt) {
yield elelemt;
elelemt = elelemt.firstElementChild;
while (elelemt) {
yield* DomTaversal(elelemt);
elelemt = elelemt.nextElementSibling;
}
}

/**
* 普通递归函数
* @param {*} elelemt
* @param {*} callback
*/
function TranversalDOM(elelemt, callback) {
callback(elelemt);
elelemt = elelemt.firstElementChild;
while (elelemt) {
TranversalDOM(elelemt, callback);
elelemt = elelemt.nextElementSibling;
}

}

const root = document.getElementById('root');

TranversalDOM(root, (elelemt) => {
console.log(elelemt);
});

for (let elelemt of DomTaversal(root)) {
console.log(elelemt);
}

生成器作为默认迭代器

因为生成器对象实现了 Iterable接口,而生成器函数和默认迭代器被调用之后都产生迭代器,所以生成器也很适合作为默认迭代器。

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);
}

与生成器进行交互

yield关键字除了可以作为函数中间返回语句使用,yield 还可以作为函数的中间参数使用。也就是说除了可以通过yield 关键字接收从生成器返回的值,还可以向生成器发送值,从而实现双向通信。

📌第一次调用 next() 传入的值不会被使用,因为这次调用是为了开始执行生成器函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function* generatorFn(initial) {
console.log(initial); // foo
const result = yield `A ${initial}`;
console.log(result); // baz
console.log(yield `B ${initial} ${result}`); // qux
}

let gen = generatorFn('foo');
// 无法通过第一次 next 方法来向生成器传递值
// 但是可以通过向生成器传递一个初始值
const result1 = gen.next('bar');
const result2 = gen.next('baz');
const result3 = gen.next('qux');
console.log(result1); // {value: 'A foo', done: false}
console.log(result2); // {value: 'B foo baz', done: false}
console.log(result3); // {value: undefined, done: true}
// foo
// baz
// qux
// {value: 'A foo', done: false}
// {value: 'B foo baz', done: false}
// {value: undefined, done: true}

提前终止生成器

与迭代器类似,生成器也有“可关闭”的概念。在生成器中return()throw() 方法都可以用于强制生成器进入关闭状态。

📌与同样实现了 Iterable接口的迭代器对象相比:迭代器的return()方法是可选的,会在提前终止迭代器时被执行。而生成器的对象都有return()方法,而且可以通过它将生成器对象进入关闭状态。

return()方法

return() 方法会强制生成器进入关闭状态。提供给 return() 方法的值就是终止迭代器对象的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function* generatorFn() {
yield* [1, 2, 3];
}

const g = generatorFn();
console.log(g); // generatorFn {[[GeneratorState]]: 'suspended'}
// 支持迭代的语言结构会忽略状态为:done:true 的 IteratorResult 内部返回的值
console.log(g.return(4)); // {value: 4, done: true}
console.log(g); // generatorFn {[[GeneratorState]]: 'closed'}

const g2 = generatorFn();
for (let x of g2) {
if (x > 1) {
// break 也可以终止迭代
g2.return(4);
}
console.log(x);
}
// 1,2

console.log(g2); // generatorFn {[[GeneratorState]]: 'closed'}
console.log(g2.next()); // {value: undefined, done: true}

throw() 方法

throw() 方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器就会关闭。

1
2
3
4
5
6
7
8
9
10
11
12
function* generatorFn() {
yield* [1, 2, 3];
}

const g = generatorFn();
console.log(g); // generatorFn {[[GeneratorState]]: 'suspended'}
try {
g.throw('error');
} catch (e) {
console.log(e) // error
}
console.log(g); // generatorFn {[[GeneratorState]]: 'closed'}

如果生成器函数内部处理了这个错误,那么生成器就不会关闭,而且还可以恢复执行。错误处理会跳过对应的 yield,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* generatorFn() {
for (const x of [1, 2, 3]) {
try {
yield x;
} catch (e) { }
}
}

const g = generatorFn();
console.log(g); // generatorFn {[[GeneratorState]]: 'suspended'}
// 要先执行,后面才能捕获错误
console.log(g.next()); // {value: 1, done: false}
g.throw('error');
console.log(g.next()); // {value: 3, done: false}
console.log(g); // generatorFn {[[GeneratorState]]: 'closed'}

📌注意:如果生成器还没有开始执行就调用 throw() 抛出错误不会在函数内部被捕获,因为这相当于在函数块外部抛出了错误。

参考

迭代器(Iterators)和生成器(Generator)

http://gyzhao.me/2020/07/13/iterators-generator/

Author

gyzhao

Posted on

2020-07-13

Updated on

2022-08-23

Licensed under

Comments