笔记为《 JavaScript 高级程序设计》第四版第 7 章的内容,简单学习了一下 ES6 的两个新增高级特性(迭代器和生成器)。
迭代( iteration )的意思是”重来“或”再来“。在软件开发领域则是按照顺序反复多次执行一段程序的意思,通常需要有明确的终止条件。ES6 新增的迭代器(iterator)和生成器(generator) 两个高级特性可以更加清晰、高效、方便地实现迭代。
1、较早的迭代是使用循环或其他辅助结构(例如 Array.prototype.forEach() )进行。
let collection = ['tkop1', 'tkop2', 'tkop3'];
for (let index = 0; index < collection.length; index++) {
console.log(collection[index];
}
2、使用 ES6 特定结构
let collection = ['tkop1', 'tkop2', 'tkop3'];
collection.forEach((item, index, arr) => console.log(item));
1、理解可迭代对象
2、迭代器的概念
迭代器是按需创建的一次性对象(理解:记录迭代过程中某时刻迭代状态的对象)。每个迭代器都会关联一个可迭代对象(实现了 iterable 接口(可迭代协议)的数据结构)并暴露该对象迭代相关的 API。迭代器无需了解其关联对象的结构,只需要知道怎么取得连续的值。
实现 iterable 接口的要求:
实现了iterable 接口的内置类型有字符串、数组、映射、集合、arguments 对象、NodeList 等 DOM 集合类型。
// 检查某数据结构是否存在默认迭代器属性可以通过检查是否暴露 Symbol.iterator 工厂函数
let num = 404;
let obj = { name: 'tkop404' };
console.log(num[Symbol.iterator]); // undefined
console.log(obj[Symbol.iterator]); // undefined
let str = '404';
let arr = ['tkop301', 'tkop302', 'tkop303'];
let map = new Map().set('a', 1);
let set = new Set().add('a');
console.log(str[Symbol.iterator]); // [Function: [Symbol.iterator]]
console.log(arr[Symbol.iterator]); // [Function: values]
console.log(map[Symbol.iterator]); // [Function: entries]
console.log(set[Symbol.iterator]); // [Function: values]
// 调用该工厂函数返回实现了 iterator 接口的对象。
console.log(str[Symbol.iterator]()); // StringIterator {}
console.log(arr[Symbol.iterator]()); // Array Iterator {}
console.log(map[Symbol.iterator]()); // MapIterator {'a' => 1}
console.log(set[Symbol.iterator]()); // SetIterator {'a'}
实现可迭代协议的所有类型都会自动兼容接收可迭代对象的任何语言特性(不需要显式调用工厂函数生成迭代器)。这些原生语言结构会在后台调用提供的可迭代对象的这个工厂函数创建一个迭代器。
// 接收可迭代对象的原生语言特性包括:
// for-of循环、数组结构、扩展运算符、Array.from()方法、创建集合、创建映射
// Promise.all() 和 Promise.race() 接收期约组成的可迭代对象。yield* 操作符
let arr = ['tkop301', 'tkop302', 'tkop303'];
// for-of 遍历
for (let item of arr) {
console.log(item); // tkop301 tkop302 tkop303
}
// 数组结构
let [name1, name2, name3] = arr;
console.log(name2); // tkop302
// 扩展操作符
let myName = [...arr];
console.log(myName); // [ 'tkop301', 'tkop302', 'tkop303' ]
// Array.from()
let arr0 = Array.from(arr);
console.log(arr0); // [ 'tkop301', 'tkop302', 'tkop303' ]
// Set 构造函数
let set = new Set(arr);
console.log(set); // Set(3) { 'tkop301', 'tkop302', 'tkop303' }
// Map 构造函数
let pairs = arr.map((item, index) => [item, index]);
console.log(pairs); // [ [ 'tkop301', 0 ], [ 'tkop302', 1 ], [ 'tkop303', 2 ] ]
let map = new Map(pairs);
console.log(map); // Map(3) { 'tkop301' => 0, 'tkop302' => 1, 'tkop303' => 2 }
迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器(对象)使用 next() 方法遍历可迭代对象中的数据。迭代器对象的几个重要特征。
1、迭代器每次调用 next() 方法都会返回一个 iteratorResult 对象,通过该对象可以知道迭代器的当前位置(迭代器返回的下一个值)。iteratorResult 对象包含 done 和 value 属性,done 属性的属性值为布尔值,表示是否还可以调用 next() 获取下一个值;value 保存可迭代对象的下一个值,如果 done 为 false(可迭代数据结构已被耗尽),则 value 值为 undefined。
let arr = ['tkop301', 'tkop302', 'tkop303'];
let iter = arr[Symbol.iterator]();
console.log(iter.next()); // { value: 'tkop301', done: false }
console.log(iter.next()); // { value: 'tkop302', done: false }
console.log(iter.next()); // { value: 'tkop303', done: false }
console.log(iter.next()); // { value: undefined, done: true }
console.log(iter.next()); // { value: undefined, done: true }
2、每个迭代器都表示对可迭代对象的一次性有效遍历。各迭代器是没有联系的,他们独立地遍历可迭代对象。他们也不知道怎么获取下一个值或者可迭代对象有多大。
3、迭代器并不与可迭代对象的某个时刻快照绑定(动态),而仅仅是通过使用游标来记录遍历的历程。
let arr = ['tkop301', 'tkop302', 'tkop303'];
let iter = arr[Symbol.iterator]();
console.log(iter.next()); // { value: 'tkop301', done: false }
// 迭代期间可迭代对象被修改,迭代器也会反映相应的变化。
arr[1] = 'tkop';
console.log(iter.next()); // { value: 'tkop302', done: false }
console.log(iter.next()); // { value: 'tkop303', done: false }
console.log(iter.next()); // { value: undefined, done: true }
任何实现 iterator 接口的对象都可以作为迭代器使用。下面 Counter 类只能被迭代一定的次数。
class Counter {
constructor(limit) {
this.count = 1;
this.limit = limit;
}
// 可迭代对象只能够创建一个迭代器(具体体现为使用结果)
next() {
return this.count <= this.limit ? { value: this.count++, done: false } : { value: undefined, done: true };
}
[Symbol.iterator]() {
// 指向迭代器本身,和原生的迭代器实现一样
return this;
}
}
let counter = new Counter(3);
// 如果解开这里的 for 遍历,下面的所有next()方法返回均为{ value: undefined, done: true }
// for (let i of counter) {
// console.log(i);
// }
let iter1 = counter[Symbol.iterator]();
let iter2 = counter[Symbol.iterator]();
console.log(iter1.next()); // { value: 1, done: false }
console.log(iter2.next()); // { value: 2, done: false }
console.log(iter1.next()); // { value: 3, done: false }
从上面的代码示例可以看出这个可迭代对象只能够创建一个迭代器,没有实现多个独立的迭代器,将 for-of 遍历部分的代码解开后,这个特点更加明显(这个迭代对象只会被遍历一次)。为了让一个迭代对象能够创建多个迭代器,就必须每创建一个迭代器就对应一个独立的计数器。为此可以使用闭包。
class Counter {
constructor(limit) {
this.count = 1;
this.limit = limit;
}
[Symbol.iterator]() {
let count = 1,
limit = this.limit;
return {
next() {
return count <= limit ? { done: false, value: count++ } : { done: true, value: undefined };
},
};
}
}
let counter = new Counter(3);
for (let i of counter) {
console.log(i);
}
let iter1 = counter[Symbol.iterator]();
let iter2 = counter[Symbol.iterator]();
console.log(iter1.next());
console.log(iter2.next());
console.log(iter1.next());
但是这个自定义的迭代器对象跟原生的迭代器对象还是存在不同,它创建出来的迭代器没有实现 iterable 接口(调用 Symbol.iterator 后返回的迭代器对象没有 Symbol.iterator 属性,没有返回相同的迭代器)。
let arr = ['tkop301', 'tkop302', 'tkop303'];
let iter1 = arr[Symbol.iterator]();
console.log(iter1.next());
let iter2 = iter1[Symbol.iterator]();
console.log(iter2.next());
// console.log(iter1 === iter2); // true
let counter = new Counter(3);
let iter3 = counter[Symbol.iterator]();
console.log(iter3.next());
let iter4 = iter3[Symbol.iterator](); // 报错,具体看下图
console.log(iter4.next());
迭代器对象中的 return() 方法用于指定在迭代器提前关闭时执行的逻辑。执行迭代的结构在想让迭代器知道他不想遍历到可迭代对象耗尽时,就可以“关闭”迭代器。可能的情况有:
class Counter {
constructor(limit) {
this.limit = limit;
}
[Symbol.iterator]() {
let count = 1,
limit = this.limit;
return {
next() {
return count <= limit ? { value: count++, done: false } : { value: undefined, done: true };
},
return() {
console.log('我要提前结束迭代');
return { done: true };
},
};
}
}
let counter = new Counter(3);
// 结果为1、2、'我要提前结束迭代'
for (let i of counter) {
if (i > 2) break; // if (i > 2) return
console.log(i);
}
try {
for (let i of counter) {
if (i === 3) throw 'err';
console.log(i);
}
} catch (e) {}
// 输出:'我要提前结束迭代'、 1、2
let [a, b] = counter;
console.log(a, b);
注意:如果迭代器没有关闭,则还可以继续从上次离开的地方继续迭代。例如数组的迭代器是不能关闭的。
let arr = ['tkop301', 2, 3, 4, 5, 6];
let iter = arr[Symbol.iterator]();
iter.return = function () {
console.log('停止遍历');
return { done: true };
}
for (let i of iter) {
console.log(i); // 'tkop301',2,3
if (i > 2) break; // '停止遍历'
}
console.log(iter.next()); // { value: 4, done: false }
for (let i of iter) {
console.log(i); // 5,6
}
因为 return() 方法是可选的,所以并非所有的迭代器都是可关闭的。要知道某个迭代器是否可关闭,可以测试这个迭代器实例的 return 属性是不是函数对象。不过,仅仅给一个不可关闭的迭代器增加这个方法并不能让它变成可关闭的。这是因为调用 return() 不会强制迭代器进入关闭状态。即便如此,return() 方法还是会被调用。
生成器是 ES6 新增的一个极为灵活的结构,拥有在一个函数块内暂停和恢复代码执行的能力。这种新能力具有深远的影响,比如,使用生成器可以自定义迭代器和实现协程。
生成器的形式是一个函数,声明函数时在函数名称的前面加一个星号(*)则表示它是一个生成器(生成器函数)。只要可以定义函数的地方就可以定义生成器(不能使用箭头函数)。并且标识生成器函数的星号不受两侧的空格影响。
// 生成器函数声明
function* generatorFn() {};
// 生成器函数表达式的形式
let generatorFn = function* () {};
// 作为对象字面量方法的生成器函数
let obj = {
*generatorFn() {}
}
// 作为实例方法的生成器函数
class MyObj {
*generatorFn() {}
}
// 作为静态方法的生成器函数
class MyObj {
static *generatorFn() {}
}
1、调用生成器函数并不会执行函数体代码,但是会返回一个与之关联的生成器对象。该生成器对象一开始处于暂停执行的状态。
2、生成器对象与迭代器对象相似,也实现了 iterator 接口,因此具有 next() 方法。调用 next() 开始执行生成器函数的函数体,并使得生成器变成下一个状态。
3、生成器调用 next() 方法的返回值与迭代器类似。done 属性表示生成器的状态,函数体执行完成(return)时,它的属性值会变为 true。故函数体为空(或者没有特定中断指令)的生成器函数中间不会停留,调用一次 next() 就会让生成器对现象状态变为 done: true。
4、value 属性则是生成器函数当前状态下的返回值,没有则默认返回 undefined 。
5、生成器对象实现了 iterator 接口,他们默认的迭代器是自引用。
function* generatorFn() {
console.log('初次调用不会执行函数体,只会返回一个生成器对象');
return 'tkop';
}
let generatorObj = generatorFn(); // 这里没有任何控制台打印内容
console.log(generatorObj); // generatorFn {}
console.log(generatorObj.next());
// '初次调用不会执行函数体,只会返回一个生成器对象'
// {value: 'tkop', done: true}
let iteratorObj = generatorObj[Symbol.iterator]();
console.log(generatorObj === iteratorObj); // true
前面在 next() 方法的返回部分提到生成器函数体内部没有中断指令时,执行 next() 不会在该函数内部停留。现在 yield 关键字则可以让生成器停止或者执行(实现生成器函数执行过程中在特定位置中断执行或者接着继续执行)。
生成器函数在生成器调用 next() 方法后,在遇到 yield关键字之前会正常执行。遇到这个关键字后则会停止执行,函数作用域状态会被保留。停止执行的生成器函数会在生成器对象再次调用 next() 方法后恢复执行。
function* generatorFn() {
console.log('generatorExe');
yield '1';
console.log('1-2');
yield '2';
console.log('2-3');
return '3';
}
let generatorObject = generatorFn();
let generatorObject2 = generatorFn();
console.log(generatorObject.next()); // generatorExe { value: '1', done: false }
console.log(generatorObject.next()); // '1-2' { value: '2', done: false }
console.log(generatorObject2.next()); // generatorExe { value: '1', done: false }
console.log(generatorObject.next()); // '2-3' { value: '3', done: false }
console.log(generatorObject.next()); // { value: undefined, done: true }
1. yield 关键字有点像函数的中间返回语句,后面跟着的值会作为 next() 方法调用返回的对象的 value 属性值。
2. 通过 yield 关键字退出的生成器函数会处于 done: false 状态,通过 return 关键字退出的生成器函数则会处于 done: true 的状态。
3. 生成器函数的内部执行流程会针对每个生成器对象区分作用域,在一个生成器对象上调用 next() 不会影响其他的生成器(生成器对象和迭代器对象一样都是独立的)。
1、在生成器对象上显式调用 next() 方法的用处并不大。其实可以将生成器当成可迭代对象使用。在需要自定义迭代对象时,这样使用生成器对象会特别有用。例如我们需要定义一个可迭代对象,它会产生执行指定次数的迭代器。使用生成器则可以通过一个简单的循环来实现。
function* generatorFn() {
yield 1;
yield 2;
yield 3;
}
let generatorObj = generatorFn();
for (let item of generatorObj) {
console.log(item);
} // 1 2 3
// 将生成器对象看作一个可迭代对象,遍历时每项均为 undefined
function* nTimes(n) {
while(n--) {
yield;
}
}
for (let _ of nTimes(3)) {
// 执行3特定迭代
}
// 遍历时每项都有匹配的值
function* range(start, end) {
while (end > start) {
yield start++; // 注意是后置自增
}
}
for (let _ of range(3, 7)) {
console.log(_); // 3 4 5 6
}
console.log(Array.from(range(2, 8))); // [ 3, 4, 5, 6, 7, 8 ]
2、使用 yield 实现输入和输出。yield 关键字作为输出上面已经涉及到了。除此之外 yield 关键字还可以作为函数的中间参数使用。上一次使生成器函数暂停的 yield 关键字会接收到传给(为恢复执行生成器函数而调用的) next() 方法的第一个值。
function* generatorFn(initial) {
console.log(initial);
console.log(yield 'tkop1');
console.log(yield 'tkop2');
}
let generatorObject = generatorFn('generatorExe');
// 第一次调用开始执行生成器函数打印 'generatorExe' (initial),遇到第一个 yield 暂停
// { value: 'tkop1', done: false } 是当前生成器对象的状态
console.log(generatorObject.next('yield1'));
// 第一个 yield 接收值 'yield2' 并开始执行第二个打印(打印第一个yield)为 'yield2'
// 遇到第二个 yield 暂停执行,打印出当前状态为{ value: 'tkop2', done: false }
console.log(generatorObject.next('yield2'));
// 第二个 yield 接收值 'yield3' 并开始执行第三个打印(打印第二个yield)为 'yield3'
// 默认执行 return,打印出当前状态为{ value: undefined, done: true }
console.log(generatorObject.next('yield3'));
// 同时用于输入和输出
function* generatorFn1() {
// 等价于 yield tkop 然后将yield的值返回
return yield 'tkop';
}
3、产生可迭代对象
可以使用星号增强 yield 的行为,让它能够迭代一个可迭代对象,从而一次产出一个值。实际上 yield* 只是将一个可迭代对象序列化为一连串可以单独产出的值,这跟将 yield 放到循环里面没有什么不同。需要注意的是 yield* 的最终值,它的值是关联迭代器返回 done: true 时的 value 属性值。
// yield* 用于迭代一个可迭代对象
function* genertorF() {
for (let _ of [1, 2, 3]) {
yield _;
}
}
function* genertorF() {
yield* [1, 2, 3];
// yield* [4, 5]; // 可以继续迭代
}
for (let _ of genertorF()) {
console.log(_);
}
// yield* 的值的问题
function* getIterator() {
yield 'tkop1';
return 'tkop2';
}
function* generatorFn1() {
console.log('yield vaule: ', yield* [1, 2, 3]);
}
function* getIteratorFn2() {
console.log('yield vaule: ', yield* getIterator());
}
for (let _ of generatorFn1()) {
console.log(_);
}
for (let _ of getIteratorFn2()) {
console.log(_);
}
4、使用 yield* 实现递归算法
yield* 最有用的地方是实现递归操作,此时生成器可以产生自身。
function* nTimes(n) {
if (n > 0) {
yield* nTimes(n - 1);
yield n - 1;
}
}
for (let _ of nTimes(3)) {
console.log(_);
}
// 0, 1, 2
每个生成器首先会从新创建的生成器对象产出每个值,然后再产出一个整数。结果就是生成器函数会递归地减少计数器值,并实例化另外一个生成器对象。从顶层来看,这就相当于创建了一个可迭代对象并返回递增的整数。
上面的示例只是简单的示范,接下来运用递归来实现图数据结构的遍历。首先实现一个图数据结构,通过随机连接节点生成一个随机的双向图(书中最终给出的图数据有误,不知道是不是作者随便编的数据还是无意弄错)。然后通过递归检查生成的图是否连通(有没有不可到达的节点)。
// 节点类对象
class Node {
constructor(id) {
// id 属性用于标识节点
this.id = id;
// 与该节点邻接的节点的集合
this.neighbors = new Set();
}
connect(node) {
if (node === this) return;
this.neighbors.add(node);
node.neighbors.add(this);
}
}
class RandomGraph {
constructor(size) {
this.nodes = new Set();
for (let i = 0; i < size; i++) {
this.nodes.add(new Node(i));
}
// 随机连接节点
const threshold = 1 / size;
for (let n1 of this.nodes) {
for (let n2 of this.nodes) {
if (Math.random() < threshold) {
n1.connect(n2);
}
}
}
}
// 这个方法仅用于调试
print() {
for (let node of this.nodes) {
const ids = [...node.neighbors].map(neighbor => neighbor.id).join(',');
console.log(`${node.id}: ${ids}`);
}
// 为后面方便使用,返回该图
return this;
}
// 检查测试该随机生成的图是否连通
isConnected() {
// 已访问节点的集合
const visitedNodes = new Set();
function* traverse(nodes) {
for (let node of nodes) {
if (!visitedNodes.has(node)) {
// 已访问集合中没有该节点
yield node;
// 递归遍历该节点的相邻节点集合
yield* traverse(node.neighbors);
}
}
}
// 取得所有节点集合中的第一个节点
let firstNode = this.nodes[Symbol.iterator]().next().value;
// 将包含第一个节点的数组作为生成器函数的可迭代对象参数
for (const node of traverse([firstNode])) {
visitedNodes.add(node);
}
return visitedNodes.size === this.nodes.size;
}
}
console.log(new RandomGraph(6).print().isConnected());
如下为书中给出的输出示例,双向图不应该出现该结果。第二张是我随机生成的两个图数据。第一个图是连通的,第二个不是连通的。
我们结合数据分析一下递归迭代的过程。
图1:
=> 随机将某个节点包含于数组中(该数组即为传给生成器函数的可迭代对象)。
=> 开始遍历其中的节点。
=> step0: 将 0 节点放入已访问的集合 => 将与其邻接的节点集合作为可迭代对象传入生成器函数(用于遍历,这是第一次执行递归) => 遍历 {1, 2, 5, 4} 这几个节点(第一层)。
=> 例如将节点 1 放入已访问集合,重复一次 step0 => 递归继续迭代他们邻接节点集合 {0, 5} => 0 在已访问集合没有再次作为迭代值返回(第二层)。
=> 将节点 5 放入已访问集合。重复一次 step0 ,此时已访问集合为 {0, 1, 5} ,作为下次传入生成器函数的可迭代对象是 {0, 2, 4, 1} (第三层),此时第二层已结束。
=> 继续递归这两个集合分别是 {0, 1, 5, 2} {0, 5} 至此这条链上的递归结束了(0, 5 节点均在已访问节点集合)。
=> 接着另一条递归链是 {0, 2, 4, 1} 中的节点 4 。
=> 此时已访问集合为 {0, 1, 5, 2, 4} ,作为下次传入生成器函数的可迭代对象是 {3, 0, 5} (第四层)。
=> 此时已访问集合为 {0, 1, 5, 2, 4, 3} ,作为下次传入生成器函数的可迭代对象是 { 4 } (节点 4 已访问,结束) 。
=> {3, 0, 5} 中节点 0 和节点 5 也已访问,第四层也结束 。回到第三层中 {0, 2, 4, 1} 只剩节点 1 也直接结束。
=> 第二层也已经结束,那么第一层 {1, 2, 5, 4} 中的节点 1 开头的递归链条就结束了,后面的三个节点都已访问过,所以整个程序执行完毕,已访问集合为 {0, 1, 5, 2, 4, 3},大小与图中所有节点数量相同,所以是相通的(随机的节点作为起点所有节点均可以访问到)。
程序的整个执行过程很复杂,但其核心点就是通过邻接关系把所有能够访问的节点放入集合中,如果图中有节点无法访问则不连通。当一个节点已经访问过了,递归链就不能往回去的方向继续进行了,这是控制递归结束的条件(在图中的直观体现是由节点 0 可以访问到 1、2、4、5 然后通过 节点 4 又可以邻接到节点 3,至此就结束了)。更直接判断有没有连通的方式是取所有节点的邻接节点集合的并集。
图2:如果将节点 0 作为起点,这个比较简单。(将节点 0 放入已访问节点集合,它没有邻接的节点,直接结束递归)。不妨将节点 1 作为起点得到的可已访问节点集合是 {1, 2, 5, 4}。
for-of 遍历会调用默认的迭代器。若将默认的迭代器定义为一个生成器函数,则会返回一个生成器对象,而生成器对象是实现了 iterator 接口的,所以完全可以充当迭代器对象在迭代中使用。
class MyArr {
constructor() {
this.values = [1, 2, 3];
}
*[Symbol.iterator]() {
yield* this.values;
}
}
for (let _ of new MyArr()) {
console.log(_);
}
生成器对象除了具有 next() 方法外,也具有 return() 方法用于提前终止生成器。此外还具有 throw() 方法。
1、生成器的 return() 方法
function* genertorF() {
for (let _ of [1, 2, 3]) {
yield _;
}
}
let g = genertorF();
for (let _ of g) {
if (_ > 1) console.log(g.return(4));
console.log(_);
}
console.log(g.next());
for (let _ of g) {
console.log(_);
}
2、生成器的 throw() 方法