Iterator and Generator
Iteration
迭代的英文 iteration 来自拉丁文 itero,意思是 ⁄“重复”或“再来”。在软开领域,迭代就是按照顺序反复多次执行一段程序,通常有明确的终止条件
- 循环是一种基础的迭代,可以指定迭代次数(有明确的终止条件),以及每次迭代要执行什么操作(反复多次执行一段程序)
- 迭代通常在一个有序集合上进行,有序集合中的所有项都可以按照既定的顺序被遍历。数组是 js 中最典型的有序集合,因而我们经常会把迭代与数组遍历混为一谈,实际二者是包含的关系,数组遍历是迭代的一种。
Iterator Pattern(迭代器模式)
早期版本的 ECMAScript 中,迭代必须借助循环或者其他辅助结构。随着代码量的不断增加,结构会变的越发混乱。很多语言通过原生的语言结构解决了这个问题,开发者无需事先知道如何迭代就能实现迭代操作。这个解决方案就是迭代器模式
Concept
迭代器模式描述了一个方案:Something can be described as “iterable” and can implement a formal Iterable interface and consumed by an Iterator
Iterable 既可以描述一个对象是可迭代的,也可以直接指代一个可迭代对象。可迭代对象可以理解为具有特定数据结构的对象,其满足两个条件:
- 包含的元素都是有限的
- 具有无歧义的遍历顺序
当然可迭代的这一概念并不局限于上述结构,凡是具有类似表现的,比如计数循环,都是可迭代的。
Iterable Protocol(可迭代协议)
实现可迭代协议要求同时具备两种能力:自我识别和创建 iterator Interface(迭代器接口)的能力
ECMAScript 暴露了[Symbol.iterator]的属性,该属性指向一个迭代器工厂函数。调用这个工厂函数,则可以生成一个新的迭代器
很多内置类型都实现了迭代接口,包括:String, Array, Map, Set, arguments, NodeList 等 DOM Collection。
实际开发中,并非一定要显式调用这个工厂函数来生成 iterator,实现可迭代协议的所有类型结构都会自动兼容可迭代对象:
- for...of 循环
for (const x of [0, 1, 2]) console.log(x)
- 数组结构
const [a, b] = [0, 1, 2]
- 扩展操作符
doSomeThing(...restArgs)
- Array.from
- 创建 Set
const newSet = new Set([a, b, c])
- 创建 Map
- Promise.all()
Promise.all([a, b, c])
- Promise.race()
- yield * 操作符 (在 Generator 生成器章节中讲解)
这种兼容可以沿着原型链进行追溯
Iterator Protocol(迭代器协议)
通过查看[Symbol.iterator]属性,可以获得这个工厂函数,直接调用可以生成一个一次性的迭代器
迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。
- 通过定义 next()方法来推进迭代进度。next 返回一个对象,其中包含迭代进度(done)和迭代数据(value),一旦迭代到达 done 为 true 的状态时,调用 next 方法是幂等的
- 有可选的 return 方法用来定义迭代器在特殊情况下过早结束时,调用的方法,同样需要返回一个包含迭代进度(done)的对象,需要自行定义。调用 return 并不会强制使迭代器关闭,但 return 依然会被调用。特殊情况包括:
- for...of 中通过 return/throw/break 关键字退出迭代
- 解构没有消耗全部的值
const [a, b] = [0, 1, 2]
const list = [0, 1];
let iter = list[Symbol.iterator]();
iter.next(); //{done: false; value: 0}
iter.next(); //{done: false; value: 1}
iter.next(); //{done: true; value: undefined}
iter.next(); //{done: true; value: undefined}
不同的迭代器之间并没有联系。
const list = [0, 1];
let iter1 = list[Symbol.iterator]();
let iter2 = list[Symbol.iterator]();
iter1.next(); //{done: false; value: 0}
iter2.next(); //{done: false; value: 0}
iter2.next(); //{done: false; value: 1}
迭代器内部是一个“黑箱”,既无法知道当前所处的迭代位置,也无法知道迭代对象的数据状态。他不保存被创建时的数据快照,只是通过一个指针追踪迭代进度。这意味着如果迭代对象产生了变化迭代器会实时反映这个变化。
const list = [0, 1, 2];
const iter = list[Symbol.iterator]();
iter.next(); //{done: false; value: 0}
// 插入新的值
list.splice(1, 0, 'insert');
iter.next(); //{done: false; value: 'insert'}
iter.next(); //{done: false; value: 1}
⚠️ 注意:因为迭代器内部维护着一个指向可迭代对象的引用,所以迭代器的存在会阻止垃圾回收程序回收可迭代对象
Custom Iterator(自定义迭代器)
迭代器模式会让期望迭代数据的结构自动兼容可迭代协议,因此我们可以在 class 中使用[Symbol.iterator]暴露迭代器工厂函数,从而自定义迭代器
class Counter {
constructor(limit) {
this.count = 1;
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 };
}
},
return() {
console.log('我还没迭代完呢!');
return { done: true, value: 'someValue' };
}
};
}
}
let counter = new Counter(3);
for (let i of counter) console.log(i);
//1
//2
//3
for (let i of counter) {
if (i === 2) break;
console.log(i);
}
//1
//'我还没迭代完呢!'
const [a, b] = counter;
// (非输出)a => 1; b => 2
// '我还没迭代完呢!'
Generator
Generator 是 ES6 新增的一个强大的能力,拥有在一个函数块内暂停和恢复代码执行的能力
生成器的形式是一个函数,函数名称前加一个星号(*)表示它是一个生成器(星号忽略空格)。只要是可以定义函数的地方,就可以定义生成器。函数声明,函数表达式,对象字面量的方法,class 实例/静态方法,都可以定义生成器。箭头函数不可用做生成器函数
generator object(生成器对象)
调用 generator 会产生一个生成器对象。生成器对象可以类比迭代器,也通过 next 方法推动执行,其中也包含迭代进度(done)和迭代数据(value)。
事实上,生成器对象内部实现了可迭代协议,其[Symbol.iterator]属性的工厂函数调用结果指向自身,在这种层面上可以说,生成器对象本身就是一种迭代器。
generator调用即挂起,generator object 一开始处于暂停执行(suspended)的状态。通过调用其 next 方法,让 generator 继续执行。当 next 到达 done: true 时,其 value 就是 generator 的 return 的值
function* generatorFn() {
console.log('执行了');
return 'returnValue';
}
let gen = generatorFn();
// (没有任何日志)
gen.next();
// '执行了'
// {done: true; value: 'returnValue'}
gen[Symbol.iterator]() === gen; // true
generator object 类比迭代器,generator 产生的每个 generator object 都会区分作用域,互不影响。
generator object 可以作为可迭代对象被迭代
function* generatorFn() {
yield 1;
yield 2;
yield 3;
}
for (const x of generatorFn()) {
console.log(x); // 1 2 3
}
yield 关键字
yield 是生成器最有用的地方,它赋予了函数内部暂停与继续执行的能力。生成器在 yield 之前会正常执行,遇到这个关键字后,执行会暂停,函数的作用与的状态会被保留。
yield 可以类比为“函数内部的 return”,通过 yield 退出的函数会处于 done: false 状态,yield 的值会作为 next 中 value 的值
function* generatorFn() {
yield '1st';
yield '2nd';
return 'finished';
}
let gen = generatorFn();
gen.next(); // {done: false, value: '1st'}
gen.next(); // {done: false, value: '2nd'}
gen.next(); // {done: false, value: 'finished'}
yield 既可作为输入,也可作为输出。第一次调用 next()传入的值不会被使用,因为这次调用是为了开始执行 generator
function* generatorFn() {
console.log('function start');
console.log(yield '1st');
console.log(yield '2nd');
}
let gen = generatorFn();
gen.next('1st pass'); // 'function start' //'1st'
gen.next('2nd pass'); // '2nd' // '2nd pass'
gen.next('3rd pass'); // '3rd' // '3rd pass'
yield* 迭代对象,一次产出一个值。与生成器星号类似,星号标志也是忽略空格的。而与 yield 不同,yield*不可以作为输出使用
function* generatorFn() {
// for (const x of [1,2,3]) {
// yield x;
// }
yield* [1, 2, 3];
}