本篇不会教条式地复述语法、如果你需要语法的详细请移步阮一峰老师的教程.
为什么要把Iterator和Generator合为一谈?因为Generator就是Iterator接口的实现,Generator遵循Iterator接口的规范,只是单看Generator语法其实是一件很令人混乱的事,所以对这些知识进行了一个整理。
什么是Iterator接口
Iterator翻译过来就是迭代器的意思,接口代表着对一类函数输出的规定。如果用ts去描述Iterator大概是这样的:
interface IteratorNextRet {
done: boolean,
value?: any
}
interface Iterator {
next: ():IteratorNextRet,
throw?: ():void,
return?: ():void
}
function iterator(): Iterator {
return {
next() { return { done: true } }
}
}
也就是Iterator规定了函数的输出必须是一个对象并且有一个next函数、next函数输出一个对象带有done 与value。后面的throw与return均为可选值
Iterator与for of
根据上述Iterator我们可以试着改写数据的迭代器:比如遍历器只遍历索引为奇数的值,应该这么写:
// 你可以试着把这串代码输出到控制台
var arr = [0, 1, 2, 3, 4, 5]
arr[Symbol.iterator] = function() {
// this 指向实例
const len = this.length
let idx = -1
return {
next: () => {
idx += 2
return {
value: this[idx],
done: idx >= len
}
}
}
}
for (let i of arr) {
console.log(i)
}
for of
大概做了这样的事情:生成一个新的迭代器、然后执行next,每次执行都输出value直到next返回值done为true、则停止输出value,停止迭代。
Iterator还有一些应用场景(ps:大家可以根据上面的实现去试一下以下的场景):
- 数组解构:[a1, a2] = 'hello;' // a1: h, a2: e String.prototype[Symbol.iterator]是有定义的
- 拓展运算符:var o = {} arr = [...o] // 报错Symbol.iterator未定义
- yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
Generator
开头就说了Generator 是Iterator接口的实现,上述我们用普通函数的方法重写了数组的迭代器,这里试着使用Generator语法再写一个:
var arr = [0, 1, 2, 3, 4, 5]
arr[Symbol.iterator] = function* () {
const len = this.length
for (let i = 0; i <= len; i++) {
if (i%2 === 1 && i > 0) {
yield i;
}
}
}
for (let i of arr) console.log(i) // 1, 3, 5
// 甚至你也可以这样
for (let j of arr[Symbol.iterator]()) console.log(j) // 1 3 5
代码与之前相比简洁了很多并且逻辑也简单很多,从2个代码的对比可以知道 * 与 yield语法干了什, * 标志着这个函数是一个Generator函数,并且妹执行一次衍生出一个实例相当于它给你主动new了、yield 相当于调用 Generator 衍生子类的 next 返回的 value 。
function* gen() {
var arr = [0, 1, 2]
for (let v of arr) yield v
}
var g1 = gen()
var g2 = gen()
g1 === g2 // false
g1.next() // {value: 0, done: false}
g1.next() // {value: 1, done: false}
g1.next() // {value: 2, done: false}
g1.next() // {value: undefined, done: true}
Generator的相当于一个指针、指向哪里由 yield 决定,什么时候指向由暴露出的 next 决定。当然还有其他2个api return
与throw
。return
和throw
每次执行相当于打断指针、直接调到最后面(return)或者掉到catch语句(throw),并且done
为true
。
yield*
yield* 本质其实更像语法糖。
function* foo() {
yield 1;
yield 2;
}
function* bar() {
yield 'a';
// 到这里执行foo()
yield* foo();
yield 'b';
}
for (var v of bar()) {
console.log(v) // a 1 2 b
}
// 相当于
function* bar() {
yield 'a'
for (var v of foo()) {
yield v;
}
yield 'b'
}
如何实现一个类async await的runner?
都说async await 是 Generator 的语法糖,我们试着用Generator去模拟async函数、从而可以更加细致的看2者的区别。
假设 await 后面跟的一个 promise 函数。
// 模拟一个fetch接口
function fetch(time) {
return function() {
return new Promise((resolve, reject) => {
setTimeout(resolve, time)
})
}
}
// 启动器、依次调用generator的next直到done为true
function run(g) {
const { value: ret, done } = g.next()
if (done) {
return Promise.resolve();
} else {
return ret.then(() => {
return run(g)
})
}
}
var fetch1 = fetch(1000)
var fetch2 = fetch(2000)
function* gen() {
console.log('开始执行fetch1');
yield fetch1();
console.log('开始执行fetch2')
yield fetch2();
console.log('done')
}
var g = gen();
console.log(run(g));
// Promise
// 开始执行fetch1
// 1s 后
// 开始执行fetch2
// 2s 后
// done
假设,有时候 await 后面跟的不一定是promise、很有可能是Thunk函数。Thunk函数可能长这样:$.get('xxx').done(callback)
。
// 模拟数据获取 thunk 函数
function fetch(time) {
return function() {
let cb
const ret = {
done(callback) {
cb = callback
},
exec() {
cb && cb()
}
}
setTimeout(ret.exec, time)
return ret
}
}
// 重写一个执行器
function run(g) {
const { value: ret, done } = g.next()
if (!done) {
ret.done(function() {
run(g);
})
}
}
var fetch1 = fetch(1000)
var fetch2 = fetch(2000)
function* gen() {
console.log('开始执行fetch1');
yield fetch1();
console.log('开始执行fetch2')
yield fetch2();
console.log('done')
}
var g = gen();
run(g);
// 开始执行fetch1
// 1s
// 开始执行fetch2
// 2s
// done
可以看出来写出了一个简易版的co模块。当然这里假设都比较极端,没有那么多的兼容。大家意会就好、所以如果有面试官问你async与generator的区别是什么?
async 相比 Generator 更加语义化、async 函数自动执行下面的await而Generator需要手动调用next,async 自动返回一个Promise函数而Generator返回Iterator接口。
以上.