写在前面
其实之前在黯羽轻扬:JavaScript生成器中已经总结过了,这里并非故意重复。之前是参考MDN文档做出的总结,偏重语法规则,本文根据FireFox中该特性的实现者的亲述,补充一些细节和应用场景
为了避免重复,本文不再解释语法规则(function* + yield
),语法细节请查看之前的文章
一.作用及内部原理
generator(生成器)用来创建迭代器,语法非常简洁(function* + yield
)
生成器执行yield
语句时,生成器的堆栈结构(本地变量、参数、临时值、生成器内部当前的执行位置)被移出堆栈。但生成器对象保留了对这个堆栈结构的引用(备份),所以稍后调用.next()
可以重新激活堆栈结构并且继续执行
例如:
// 定义生成器
var gen = function*() {
console.log('before yield 1');
yield 1;
console.log('before yield 2');
yield 2;
}
// 调用生成器返回迭代器
var iter = gen();
iter.next(); // before yield 1
// Object {value: 1, done: false}
iter.next(); // before yield 2
// Object {value: 2, done: false}
iter.next(); // Object {value: undefined, done: true}
iter.next(); // Object {value: undefined, done: true}
yield
语句把函数体分割成了几段,.next()
一次执行一段
二.迭代器与生成器
function*定义的东西叫迭代器的生成器(简称生成器),因为调用它返回一个迭代器
所有的生成器都有内建.next()
和[Symbol.iterator]()
方法的实现,我们只需要编写循环部分的行为。function*
后面的函数体就像循环结构的循环体,例如:
function* gen(arr) {
for (var i = 0; i < arr.length; i++) {
yield arr[i];
}
}
var iter = gen([1, 2, 4]);
console.log(iter.next()); // Object {value: 1, done: false}
console.log(iter.next()); // Object {value: 2, done: false}
其中gen
的作用就是把连续的数组变成“会喘气的”数组,for循环本来是停不下来的,但yield
确实让它停下来了,这也是生成器的一大特色。利用这个特点可以实现很多有趣的东西,比如用动画展示快速排序的过程,伪代码如下:
function quicksort(arr) {
// sort
forloop {
updateSortedArr(); // 完成一趟排序
displaySortedArr(); // 展示本趟排序结果
}
return sortedArr;
}
当然,这样无法看到动画。因为n趟排序瞬间就完成了(太快了,啥都看不清啊哪有动画啊,根本没变好吗)。很容易想到变通的方法:先把每一趟的结果存起来,最后再展示:
function quicksort(arr) {
var tmpArr = [];
forloop {
updateSortedArr(); // 完成一趟排序
tmpArr.push(sortedArr); // 存起来
}
// 动画展示排序过程
anim(tmpArr);
}
拿到每一趟的结果后,想怎么展示就怎么展示。感觉也不很费劲,那好,如果要动画展示快速排序每一趟2个指针的移动呢?我们好像又需要记录指针移动的tmpArr
了,如果这个数组很大,如果。。。这样势必需要更多的内存空间来记录已经发生过的事情
仔细想想,排序过程中,动画展示需要的数据都已经有了,那能不能边排序边动画展示呢?当然可以:
function* quicksort(arr) {
forloop {
yield sortedArr;
// 或者
// yield step;
}
}
var iter = quicksort(arr);
// 动画展示排序过程
function anim() {
display(iter.next());
setTimeout(anim, 300);
}
anim();
没错,生成器让循环能“喘气”了,在喘气过程中制造动画
P.S.其实就算没有生成器,我们也能让循环“停”下来,请查看黯羽轻扬:JavaScript实现yield
P.S.关于快速排序的具体细节,请查看黯羽轻扬:排序算法之快速排序(Quicksort)解析
三.特点
生成器的特点如下:
普通函数不能自暂停,生成器函数可以
yield
只在function*
的直接作用域中有效,function*
中匿名函数中的yield
是非法的可以处理无限序列。无法构造无限大的数组,但可以用生成器实现无限序列的构造规则,以处理无限序列
提供了返回数组的另一种思路,返回生成器而不是数组,用时间换空间
可以重构复杂循环,把它拆分成2个部分,把数据生成部分转换为生成器,然后for…of遍历这些数据
可以快速制造迭代器,让任意对象可迭代,具体请查看黯羽轻扬:for…of循环_ES6笔记1
便于扩展迭代器,很容易实现过滤等操作
第2点需要注意,因为大多数介绍生成器的资料都不会提及这一点,但我们确实无法在生成器中setTimeout
延迟yield
。用生成器扩展迭代器是个不错的选择,代码很自然,示例如下:
// 扩展迭代器
function* filter(isValid, iterable) {
for (var val of iterable) {
if (isValid(val)) {
yield val;
}
}
}
// test
function isValid(val) {
return val > 1;
}
for (var val of filter(isValid, [0, 1, 2, 4])) {
console.log(val);
}
用生成器包装迭代器,有种无缝衔接的美感,不是吗?
四.高级技巧
1.从外部影响生成器的逻辑流
迭代器的next(returnVal)方法接受可选参数,参数会作为生成器中上一条yield语句的返回值,这样调用者就可以从外部影响生成器的逻辑流,例如:
function* gen() {
var water = yield 'give me a cup of pure water';
yield water.drink();
}
// test
var iter = gen();
console.log(iter.next());
console.log(iter.next({
name: 'pure water',
drink: function() {
return 'hmm, well';
}
}));
第一个yield
向调用者要一杯水,第二个.next()
把水递给生成器了,然后第二个yield
把水喝掉了
这是一个双向的交互过程,实际应用中,根据next()
的返回值判断生成器需要什么,再通过下一个.next()
传递给它,把复杂逻辑隔离在生成器中,调用起来就像对话一样轻松
2.终止迭代器
注意,是迭代器,有两种方法终止一个迭代器:
迭代器的
throw(err)
方法,效果像是生成器中yield表达式调用一个函数并抛出错误迭代器的
return(returnVal)
方法,接受可选参数,参数会作为value返回(done
为true
),生成器只执行finally
代码块并不再恢复执行
throw
示例如下:
// throw
function* gen() {
try {
yield 1;
yield 2;
} catch (err) {
console.log('error occurs: ' + err);
} finally {
console.log('clean up');
}
}
var iter = gen();
console.log(iter.next()); // Object {value: 1, done: false}
console.log(iter.throw(new Error('err'))); // error occurs: Error: err
// clean up
// Object { value: undefined, done: true }
console.log(iter.next()); // Object {value: undefined, done: true}
throw(err)
表示迭代器异常关闭,用于通知生成器内部执行finally
中的清理工作
而return()
则表示迭代器正常关闭,示例如下:
console.log(iter.next()); // Object {value: 1, done: false}
console.log(iter.return('ok')); // clean up
// Object { value: "ok", done: true }
console.log(iter.next()); // Object {value: undefined, done: true}
注意:’ok’作为value立即返回,而不是下一个.next()
时返回
P.S.Chrome49还是不支持return()
,FF下throw()
后仍然可以return()
,而如果先return()
再throw()
会报错
3.拼接迭代器
yield* iter
可以拼接迭代器,支持在一个生成器中调用另一个生成器,例如:
var gen1 = function* (){
yield 1;
yield 2;
}
var gen2 = function* (){
yield* gen1();
yield 3;
yield 4;
}
for (var val of gen2()) {
console.log(val); // 1 2 3 4
}
五.总结
生成器能让执行流“喘口气”,能让停不下来的东西暂停,能用来重构循环,能驾驭无限序列,能包装迭代器。。。好处多多
参考资料
- 《ES6 in Depth》:InfoQ中文站提供的免费电子书