对于ES6的生成器函数总结有四点:
1. yield必须放置在*函数中;
2. 每次执行到yield时都会暂停函数中剩余代码的执行;
3. *函数必须通过函数调用的方式(new方式会报错)才能产生自身的实例,并且每个实例都互相独立;
4. 一个生成器函数一旦迭代完成,则再也无法还原,一直停留在最后一个位置;
尤其是第二点,是非常强大的功能,暂停代码执行,以前只有在浏览器环境中,alert、comfirm等系统内置函数才具有类似的能力,所以如果熟悉多线程的语言,你会找到类似的感觉,于是也有人说,有了yield,NodeJS就有协程的能力,完全可以处理多个需要协作的任务。
也是因为第二点,生成器函数也具有惰性求值的特性,针对这一特性,我们可以很容易写出科里化函数进行惰性求值,如下:
function *curr() {
let items = [],
value
do {
// 如果不需要返回值,可以直接写成value = yield
// 每次返回现有的数组元素
value = yield items.slice()
// 当等于-1时终止
if(value !== -1) {
items.push(value)
}
} while(value !== -1)
// 进行求值
let sum = 0
items.forEach(item => sum = sum + item)
yield sum
}
根据生成器函数的第二个特性,只要函数中还有yield未执行,那么剩余的代码就绝不会执行,所以上面的代码中,只要循环未完成,求值的代码就不会执行,再看相关的测试代码,如下:
let curring = curr()
// 必须要空转一次,代码启动柯里化
curring.next()
curring.next(1)
curring.next(2)
curring.next(3)
// 启动求值过程
console.log(curring.next(-1))
上面的代码有一次空转的过程,这是因为next方法的参数只可以作为上一个yield表达式的返回值,所以对所有的生成器函数而言,都无法获取到第一次的next()方法的返回值,具体的执行过程如下:
1. curring.next()空转时,执行到yield items.slice()暂停,请记住,此时还没有返回值,而且value的赋值操作还没有执行,请记住赋值语句的右侧代码先执行;
2. 继续执行curring.next(1),返回值为1,进行赋值操作,并继续剩余的循环代码,直到遇到yield才终止本次执行;
3. 继续执行第二步,直到方法结束为止;
在上面的执行结果中,我们还发现输出结果有些出乎意料,如下:
{ value: 6, done: false }
done竟然是false,这说明方法还没有执行结束,必须还要执行一次curring.next,才能终结方法,done才能变为true,这实在是太低效了,又空转了一次,那怎么改进呢?很简单,只需要将最后的yield改为return即可,如下:
// 进行求值
let sum = 0
items.forEach(item => sum = sum + item)
// 只有使用return才能终结方法
return sum
这是yield与return的区别:
1. yield仅代表本次迭代完成,并且还必有下一次迭代;
2. return则代表生成器函数完成;
最后,为了减少柯里化代码中不必要的一次空转迭代,我们用一种掩耳盗铃的方式封装构造函数,为什么是掩耳盗铃,一看就明白:
// 对生成器函数进行封装
function wrapper(fn) {
// 这里的...运算符将参数转换为数组
return function(...args) {
// 除了使用析构与扩展运算,还可以使用apply函数,如下
//let generator = fn.apply(null, args)
// 这里的...运算符是逆向运算,将数组又转换为参数列表
let generator = fn(...args)
// 将空转放到这里
generator.next()
return generator;
}
}
// 现在应用代码可以简化了
let curring = wrapper(curr)()
curring.next(1)
curring.next(2)
curring.next(3)
总结
利用生成器函数可以进行惰性求值,但无法获取到第一次next函数传入的值,而且只要执行了yield的返回操作,那么构造函数一定没有执行完成,除非遇到了显式的return语句。