迭代器
迭代器(iterator),使用户可在容器对象(container,例如链表或数组)上遍历的对象。
迭代器模式
迭代器模式可以让开发者无需了解如何迭代就可以实现迭代操作。Python,C#,Java对该模式都有完备的支持,JavaScript在ECMAScript 6 也将其引入到语言核心中来。
迭代器是一个可以由任意对象实现的接口,支持连续获取对象产出的每一个值。任何实现Iterable
接口的对象都有一个 Symbol.iterator 属性,该属性引用默认迭代器。默认迭代器就是一个工厂函数,调用后会产生一个实现 Iterator
接口的对象。若一个对象拥有迭代行为该对象就是一个可迭代对象。
可迭代协议(Iterable )和迭代器协议(Iterator)
实现Iterable
接口要求同时具备两种能力:
支持迭代自我识别能力
带 Symbol.iterator 键(key)的属性,暴露该属性作为”默认迭代器
“,并引用一个迭代器工厂函数
创建Iterator
接口的对象的能力
一个迭代器工厂函数,调用这个工厂函数必须返回一个迭代器
迭代器协议(Iterator
)是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器API 使用next() 方法再可迭代对象中遍历数据。
实现了Iterable接口的内置类型
字符串
数组
映射
集合
arguments
对象
NodeList
等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
| let num = 1; let obj = {};
console.log(num[Symbol.iterator]); console.log(obj[Symbol.iterator]);
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]); console.log(arr[Symbol.iterator]); console.log(map[Symbol.iterator]); console.log(set[Symbol.iterator]); console.log(element[Symbol.iterator]);
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); console.log(arrIterator); console.log(mapIterator); console.log(setIterator); console.log(elementIterator);
|
可迭代对象的语法
对于可接受迭代对象的原生语言特性而言,实际编码过程中不需要显式调用这个工厂函数来生成迭代器,这些原生语言结构会在后台调用提供的可迭代对象的这个工厂函数,从而创建一个迭代器。
支持可迭代对象的原生语言结构包括:
如下所示:
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);
let arr2 = [...arr]; console.log(arr2);
let arr3 = Array.from(arr); console.log(arr3);
let set = new Set(arr); console.log(set);
let pairs = arr.map((el, i) => [i, el]); let map = new Map(pairs); console.log(pairs); console.log(map);
|
自定义迭代器
与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]() { 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; } }
console.log(counter[Symbol.iterator]()); let counter2 = new Counter(5); console.log(counter1[Symbol.iterator] === counter2[Symbol.iterator]);
|
提前终止迭代器
可选的 return()
方法(只能返回: {done:true}
)用户指定在迭代器提前关闭时执行的逻辑。而执行迭代的结构想让迭代器提前关闭,可能包括的情况有:
如下所示:
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); }
let counter2 = new Counter(5); try { for (let i of counter2) { if (i > 2) { throw new Error('Error'); } console.log(i); } } catch (e) { }
let counter3 = new Counter(5); let [a, b] = counter3;
|
如果迭代器没有关闭,那么还可以继续从上次离开的地方继续迭代。数组的迭代器就是不能关闭的:
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; }
for (let i of iter) { console.log(i); }
|
生成器函数
生成器是一种特殊的函数。最初调用时,生成器函数不执行任何代码,调用之后会返回一个生成器对象(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); console.log(gen.next()); console.log(gen.next()); console.log(gen); console.log(gen.next()); console.log(gen);
console.log(gen[Symbol.iterator]() === gen);
|
生成器对象作为可迭代对象
因为生成器对象也实现了 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; console.log(yield* generatorStr()) yield* [3, 4]; }
function* generatorStr() { yield 'a'; yield 'b'; return 'c'; }
for (let i of generatorNum()) { console.log(i); }
|
使用生成器
使用生成器生成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);
|
使用 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
|
function* DomTaversal(elelemt) { yield elelemt; elelemt = elelemt.firstElementChild; while (elelemt) { yield* DomTaversal(elelemt); elelemt = elelemt.nextElementSibling; } }
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); const result = yield `A ${initial}`; console.log(result); console.log(yield `B ${initial} ${result}`); }
let gen = generatorFn('foo');
const result1 = gen.next('bar'); const result2 = gen.next('baz'); const result3 = gen.next('qux'); console.log(result1); console.log(result2); console.log(result3);
|
提前终止生成器
与迭代器类似,生成器也有“可关闭”的概念。在生成器中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);
console.log(g.return(4)); console.log(g);
const g2 = generatorFn(); for (let x of g2) { if (x > 1) { g2.return(4); } console.log(x); }
console.log(g2); console.log(g2.next());
|
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); try { g.throw('error'); } catch (e) { console.log(e) } console.log(g);
|
如果生成器函数内部处理了这个错误,那么生成器就不会关闭,而且还可以恢复执行。错误处理会跳过对应的 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);
console.log(g.next()); g.throw('error'); console.log(g.next()); console.log(g);
|
📌注意:如果生成器还没有开始执行就调用 throw()
抛出错误不会在函数内部被捕获,因为这相当于在函数块外部抛出了错误。
参考