JS生成器和迭代器
迭代器
为什么会说到这个呢?原因就是从扩展运算符和for…of的使用中发现这个问题的
let b = [1,2,3,4]
Math.max(...b)
=>4
let a ={1:2,5:6,8:9}
Math.max(...a)
=>Uncaught TypeError: a is not iterable
嗯,我们从提示上知道a并不是可迭代的,那怎么做呢?看一下数组上的原型方法
MDN对于迭代器的描述:
在 JavaScript 中,迭代器是一个对象,它定义一个序列,并在终止时可能返回一个返回值。 更具体地说,迭代器是通过使用
next()
方法实现 Iterator protocol 的任何一个对象,该方法返回具有两个属性的对象:value
,这是序列中的 next 值;和done
,如果已经迭代到序列中的最后一个值,则它为true
。如果value
和done
一起存在,则它是迭代器的返回值。
a[Symbol.iterator]=function(){
let keys = Object.keys(a);
let len = keys.length;
let values = Object.values(a);
let n = 0;
return {
next: function() {
if (n < len) {
return {
value: {k: keys[n], v: values[n++]},
done: false
}
} else {
return {
done: true // 注意,一旦 done 为 true ,此时的 value 不返回
}
}
}
}
}
// 此时打印
console.log(...a)
=>Object { k: "1", v: 2 }
Object { k: "5", v: 6 }
Object { k: "8", v: 9 }
String
、Array
、TypedArray
、Map
和Set
都是内置可迭代对象,因为它们的原型对象都拥有一个Symbol.iterator
方法。
记下内置对象谁是可迭代的,明显没有Object。
做完自定义迭代器之后就会发现,这个next()的用跟某某某很像啊,会不会有什么关联呢??
先来看看它是谁:
生成器
没错生成器,一个es6很难懂的特性(还有promise),当然其实它es5就提出来了。
再来看看MDN的解释:
虽然自定义的迭代器是一个有用的工具,但由于需要显式地维护其内部状态,因此需要谨慎地创建。生成器函数提供了一个强大的选择:它允许你定义一个包含自有迭代算法的函数, 同时它可以自动维护自己的状态。 生成器函数使用
function*
语法编写。 最初调用时,生成器函数不执行任何代码,而是返回一种称为Generator的迭代器。 通过调用生成器的下一个方法消耗值时,Generator函数将执行,直到遇到yield关键字。
的确,我们的迭代器它整个的next流程是可以由我们自己编写的,就像上面那个例子,next会有自己对应的一个返回值,在内部n++更改value并且到头之后done
经典斐波那契数列
function* fibonacci() {
var fn1 = 0;
var fn2 = 1;
while (true) {
var current = fn1;
fn1 = fn2;
fn2 = current + fn1;
var reset = yield current;
if (reset) {
fn1 = 0;
fn2 = 1;
}
}
}
var sequence = fibonacci();
console.log(sequence.next().value); // 0
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2
console.log(sequence.next().value); // 3
console.log(sequence.next().value); // 5
console.log(sequence.next().value); // 8
console.log(sequence.next(true).value); // 0
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2
yield
这里需要学一下yield关键字
yield
关键字实际返回一个IteratorResult
对象,它有两个属性,value
和done
。value
属性是对yield
表达式求值的结果,而done
是false
,表示生成器函数尚未完全完成
一旦遇到 yield
表达式,生成器的代码将被暂停运行,直到生成器的 next()
方法被调用。每次调用生成器的next()
方法时,生成器都会恢复执行,直到达到以下某个值:
yield
,导致生成器再次暂停并返回生成器的新值。 下一次调用next()
时,在yield
之后紧接着的语句继续执行。throw
用于从生成器中抛出异常。这让生成器完全停止执行,并在调用者中继续执行,正如通常情况下抛出异常一样。- 到达生成器函数的结尾;在这种情况下,生成器的执行结束,并且
IteratorResult
给调用者返回undefined
并且done
为true
。 - 到达
return
语句。在这种情况下,生成器的执行结束,并将IteratorResult
返回给调用者,其值是由return
语句指定的,并且done
为true
(这种情况要注意一下,生成器结束不代表下一次next就会继续输出返回值了,因为返回值所在的执行上下文已经被销毁我们后续的next其实都找不到value了,也就是后续都会输出undefined而不是返回值)。
嗯,四种情况,第一种类似上面数列暂停开始暂停开始这样的过程。第二种是抛出错误,第三种为没有return值,即return undefined,最后是而我们正常的return 此时IteratorResult
中的done为true;
那问题来了,return yield呢?
function *test(){
yield 1;
return yield 2;
}
var a = test()
a.next()
=>Object { value: 1, done: false }
a.next()
=>Object { value: 2, done: false }
a.next()
=>Object { value: undefined, done: true }
从结果可以看出来,先是运行yield 2 得到Object { value: 2, done: false },然后才是执行return 语句,因为yield关键字并没有返回值,此时return 出来的value就会是undefined
再看下yield委托
yield*
yield*
表达式迭代操作数,并产生它返回的每个值。
yield*
表达式本身的值是当迭代器关闭时返回的值(即done
为true
时)。
-
产生它返回的每个值
什么意思呢?意思就是yield* 后面可以跟另一个
generator
或可迭代对象,然后通过next()来获取generator
或可迭代对象他们每次迭代返回的值。这里就可迭代对象就是我们前面提到的数组,字符串,map,set等,嗯,迭代器跟生成器就能串起来用了。 -
表达式返回 关闭时返回的值
也挺好理解的,yield* 不会像yield一样没有返回值,他会返回后面的生成器或者可迭代对象done=false时的value。
不过默认情况下对于可迭代对象关闭时他提供的value都是undefined
function* g() { yield* [1, 2, 3];return 4; } var result; function* gg() { result = yield* g(); } var iterator = gg(); result =>undefined // 此时gg()函数暂停在yield*表达式也就是g()中的yield*[1,2,3]也就是yield 1这里,因此赋值语句仍然未执行,result的值为undefined iterator.next() =>Object { value: 1, done: false } iterator.next() =>Object { value: 2, done: false } iterator.next() =>Object { value: 3, done: false } iterator.next() Object { value: undefined, done: true } result =>4
此时可以看到result已经输出4了也就是前面g函数关闭返回的value,同时由于gg函数它并没有return值,迭代器结束的时候他的value是为undefined。
讲到这基本生成器用法也算是熟悉了,再来写一个挺有意思的东西
异步生成器
也就是async await 和yield组合起来的时候会发生什么事情呢?
async function* combind(){
await new Promise((resolve,reject)=>{resolve('a');}).then(console.log);
yield 1;
return 2;
}
var a = combind();
a
=>AsyncGenerator { }
控制台打印了a的类型就叫异步生成器,看来是一个特殊的东西;正常我们的generator他next返回的是一个IteratorResult
对象,也就是一个包含value跟done两个键的一个对象,那么异步生成器呢?
a.next()
=>a
=>Promise { <state>: "pending" }
a.next()
=>Promise { <state>: "pending" }
a.next()
=>Promise { <state>: "fulfilled", <value>: {<value>: {value: undefined,done: true,}}
嗯?怎么是一个Promise对象,这倒是类似await后的语句要返回一个Promise,而且这里三次next后promise的状态就转换为fullfilled了且带了个IteratorResult
?试试then打印值;
a.next().then(console.log)
=>a
=>Object { value: 1, done: false }
a.next().then(console.log)
=>Object { value: 2, done: true }
a.next().then(console.log)
=>Object { value: undefined, done: true }
//输出结果基本等于
function* combind(){
yield 1;
return 2;
}
到了这里我们豁然开朗,异步生成器的await关键字跟原来的操作还是一样,但是它得根据生成器执行到了哪里来决定本身有没有被执行。yield关键字则有较大的变化,他不再返回一个对象而是一个promise异步的迭代器,这个迭代器resolve了我们IteratorResult
对象,所以我们可以通过.then来访问结果;同时在生成器执行结束之后promise的状态将会变成fullfilled。
那么,既然它返回的是迭代器,我们能不能配合for…of来使用呢?
async function* combind(){
await new Promise((resolve,reject)=>{resolve('a');}).then(console.log);
yield '1/3';
await new Promise((resolve,reject)=>{resolve('b');}).then(console.log);
yield '2/3';
await new Promise((resolve,reject)=>{resolve('c');}).then(console.log);
yield '3/3';
}
var a =combind();
(async ()=>{for await(const i of a)console.log(i)})()
=>a
=>1/3
=>b
=>2/3
=>c
=>3/3
是的,这个样例也揭示了这个生成器非常适用于进度条问题。
总结
本文通过对生成器迭代器的语法进行了简要的学习,找出两者间的异同点,再扩展了一个异步生成器,对生成器有了新的认识。
参考文章:
MDN文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Iterators_and_Generators
异步生成器:https://segmentfault.com/a/1190000020499552