generator(生成器)_ES6笔记2

写在前面

其实之前在黯羽轻扬: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返回(donetrue),生成器只执行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中文站提供的免费电子书

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code