本文内容
迭代器
所有迭代器对象都有 next() 方法,每次返回一个结果对象,结果对象有两个属性,一个是 value,表示当前迭代的值,一个是 done,表示迭代是否已经完成。如果我们已经迭代完了最后一个元素,再次访问 next() 时,返回对象的 done 将是 true,value 则包含一个终值,这个值不是我们迭代集合的一部分,而是包含了一些其它信息,常规情况下返回的是 undefined
let m = [2, 3, 5];
let iterator = m[Symbol.iterator]();
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: 5, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
我们使用 m[Symbol.iterator](); 访问了数组的内建的迭代器。
可以看到 2,3,5 迭代完成后,再次访问 next() 才会返回 done = true,值则是 undefined
一个迭代器是一个只能消费一次的对象,我们使用一个迭代器只能从头到尾迭代一次,迭代器内部保存了迭代的状态,例如当前迭代到第几个元素了,是一种可变对象
生成器
生成器是一种返回迭代器的函数,通过在 function 关键字后面加上星号(*)来表示,在这样的函数中,可以使用 yield 关键字(没有加星号则不能使用这个关键字)
function* generator() {
console.log(1111);
yield 1;
console.log(2222);
yield 2;
console.log(3333);
yield 4;
console.log(4444);
return 5;
}
let iterator = generator();
console.log(9999);
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 4, done: false }
console.log(iterator.next()); // { value: 5, done: true }
console.log(iterator.next()); // { value: undefined, done: true }
输出是
(py3.5) czp@:~/workspace/knowledge-base/demos/node_start$ node hello.js
9999
1111
{ value: 1, done: false }
2222
{ value: 2, done: false }
3333
{ value: 4, done: false }
4444
{ value: 5, done: true }
yield 关键字
使用了 yield 关键字的函数,当我们进行调用时,就会返回一个迭代器对象。这个迭代器对象包含的值就是这个生成器函数体中,yield 的顺序。
例如上例中总共会执行 3 次 yield,分别是 1,2,4,这个生成器返回的迭代器对象就可以依次迭代出这三个值。
迭代器的执行步骤如下:
- 首先被生成器生成一个迭代器,此时,生成器函数的内部逻辑,一行代码都不执行,表现就是上例中 1111 没有在一开始就输出
- 迭代器调用
next(),此时,生成器一直执行到第一次 yield 为止,表现就是上例中输出了 1111,没有输出 2222。然后将 yield 的值作为本次返回的对象的 value - 迭代器继续调用
next(),此时,生成器从上次 yield 的下一行开始执行(上例中就是console.log(2222);),一直执行到第二次 yield 为止,然后将 yield 的值作为本次返回的对象的 value - 倒数第二次
next(),执行到了yield 4;停止,返回的是 4 - 最后一次
next(),没有碰到 yield,而是碰到了 return,宣告迭代结束,本次迭代返回的是 return 的值(如果没有 return,而是执行完了生成器的方法体,那么返回 undefined),同时 done 的值是 true
可以看出,yield 像是一个一个阶段,每次执行到 yield 就停止,下次从 yield 的下一行开始执行,一直到执行完生成器函数,就代表迭代器迭代结束了。如果我们再次调用 next() 那么依然是返回最后一次的结果。
yield 关键字只能在生成器内部使用,在其它地方使用会报错,即使是在生成器内部的函数里使用也会报错,例如
function* generator(items) { items.forEach(function (item) { yield item + 1; // 语法错误 }) }
生成器函数表达式
let generator = function* () {
console.log(1111);
yield 1;
console.log(2222);
yield 2;
console.log(3333);
yield 4;
console.log(4444);
return 5;
};
不能用箭头函数来创建生成器
对象属性里创建生成器
let obj = {
* generator() {
console.log(1111);
yield 1;
console.log(2222);
yield 2;
console.log(3333);
yield 4;
console.log(4444);
return 5;
}
};
这种是 ES 6 里在对象中创建生成器的方式
可迭代对象
可迭代对象都具有 Symbol.iterator 属性,调用该属性方法,可以返回一个作用于该对象的迭代器,例如之前的例子
let m = [2, 3, 5];
let iterator = m[Symbol.iterator]();
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: 5, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
在 ES 6 中,以下都是可迭代对象
- 数组
- Set
- Map
- 字符串
- 通过生成器创建的迭代器(生成器默认会为迭代器的 Symbol.iterator 属性赋值,所以这种迭代器本身是可以迭代的)
for-of 循环
for-of 循环每次执行,都会调用可迭代对象的迭代器的 next() 方法,一直持续到迭代器返回的 done 为 true 为止
let m = [2, 3, 5];
for (let number of m) {
console.log(number);
}
// 2 3 5
这段代码背后的逻辑
- JavaScript 引擎调用
m[Symbol.iterator]()方法生成迭代器 - 循环体中不断调用迭代器的
next()方法赋值给 number - 当
next()返回的 done 是 true 时退出循环
Symbol.iterator
上例已经说明了,可迭代对象的 Symbol.iterator 属性是一个方法,调用后可以返回可迭代对象的一个迭代器,通过这个迭代器可以访问可迭代对象的元素
let m = [2, 3, 5];
let iterator = m[Symbol.iterator]();
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: 5, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
因此我们可以使用这个属性来检测,对象是否是可迭代对象
创建可迭代对象
根据上述说明,我们可以通过给对象添加 Symbol.iterator 属性来创建可迭代对象
let obj = {
j: 100,
k: 200,
l: 300,
* [Symbol.iterator]() {
yield this.j;
yield this.k;
yield this.l;
}
};
for (let number of obj) {
console.log(number);
}
// 100 200 300
内建迭代器
ES 6 很多的可迭代对象都有一些 api 来访问内建的迭代器,不需要访问 Symbol.iterator 这么底层的迭代器
集合对象迭代器
数组、Map、Set 都内建了 3 种迭代器
- entries() 返回一个迭代器,值为多个键值对
- values() 返回一个迭代器,值为集合的值
- keys() 返回一个迭代器,值为键名
entries()
每次调用 next() 返回一个数组,数组 2 个元素分别表示 key 和 value
keys()
每次调用 next() 返回一个 key
values()
每次调用 next() 返回一个 value
字符串迭代器
ES 5 之后,字符串越来越像数组了,例如可以通过方括号访问字符
let str = 'abcd';
console.log(str[2]); // c
对于双编码单元的字符,则会出问题
let str = 'ab😁';
console.log(str[2]); // �
因为这种索引访问的是 UTF-16 编码单元,而不是单个字符。有的字符需要两个 UTF-16 编码单元来表示,因此就出错了
ES 6 添加了字符串迭代器,可以正确的访问字符
let str = 'ab😁';
for (let x of str) {
console.log(x);
}
输出
(py3.5) czp@:~/workspace/knowledge-base/demos/node_start$ node hello.js
a
b
😁
迭代器参数
我们使用 next() 方法时,还可以给迭代器传递参数,这个参数将会替代迭代器内部上一次 yield 时的返回值。
function* generator() {
let first = yield 1;
let second = yield first + 2; // 4 + 2
yield second + 3; // 5 + 3
}
let iterator = generator();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next(4)); // { value: 6, done: false }
console.log(iterator.next(5)); // { value: 8, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
执行步骤如下:
- 首先生成器生成一个迭代器
- 迭代器第一次调用
next()返回的 value 是 1,此时迭代器内部只是执行了yield 1,但是还没有执行赋值语句let first = yield 1; - 迭代器调用
iterator.next(4),此时迭代器内部的yield 1的返回值变成了我们传入的 4,因此let first = yield 1;将 first 赋值为 4,于是yield first + 2返回的 value 是 6 - 迭代器调用
iterator.next(4),此时迭代器内部的yield first + 2的返回值变成了我们传入的 5,因此let second = yield first + 2;将 second 赋值为 5,于是yield second + 3返回的 value 是 8 - 最后一次调用
next()返回的 value 是 undefined,done 是 true
迭代器的这种行为,可以被用来实现异步编程
在迭代器中抛出错误
我们可以在迭代器中抛出错误,这个错误抛出的地点就是上一次执行 yield 时
function* generator() {
let first = yield 1;
try {
let second = yield first + 2; // 此处将会抛出错误
} catch (e) {
console.log(e.message);
yield 3; // 3
}
}
let iterator = generator();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next(4)); // { value: 6, done: false }
console.log(iterator.throw(new Error('瞬间爆炸'))); // { value: 8, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
throw 也可以迭代一次进行返回