ES6 Generators 基础

原文:The Basics Of ES6 Generators https://davidwalsh.name/es6-generators  作者: Kyle Simpson on July 21, 2014 翻译:L472547033 

ES6新出了一种函数叫做generator,名字虽然有点奇怪,但是看完本篇文章你会知道它是如何工作的,并且对它在未来的强大之处有所了解。

Run-To-Completion(普通函数一直运行的特性)

通过一些示例,看看generator与普通函数有什么区别。
如下所示,运行普通函数,是一个同步运行过程,只有运行完一个函数,才能执行下一行代码。(Run-To-Comletion)
setTimeout(function(){
    console.log("Hello World");
},1);

function foo() {
    // NOTE: don't ever do crazy long-running loops like this
    for (var i=0; i<=1E10; i++) {
        console.log(i);
    }
}

foo();
// 0..1E10
// "Hello World"
通过程序输出可以看出,foo()函数要执行很长时间(非常耗时的循环),setTimeout函数表示等待1ms输出Hello world,但实际运行情况是,先运行了setTimeout函数并开始计时,然后立即执行foo()函数,foo()执行了很长时间,再此期间,已经到了setTimeout设置的1ms,却只能等待foo()函数执行完毕,才能执行setTimeout中的回调函数,输出Hello world。

如果setTimeout可以打断(interupt)foo()的执行,那么会发生什么?

这正是具有挑战性的多线程问题,不过不用担心,JavaScript是单线程的,避免了很多麻烦。

注意:Web Workers技术虽然是多线程,但对于这种被限制的多线程,也是遵守上述的阻塞特性,所以在这里无需讲解更复杂的多线程理论。

Run..Stop..Run

ES6中的generators函数就不太一样了(相对于JS普通函数),generators函数可以在执行中多次暂停,在暂停期间可执行其它代码,稍后又可恢复执行。

如果读过多线程编程的相关书籍,就会知道“多线程协作”的理论,它倡导【线程/函数】可以根据需要被打断(暂停/interupt)。

ES6中的generators函数具有多线程协作的行为,在generator函数中可以用关键字yield暂停执行(打断/interupt),只能自暂停,不能由外部代码暂停执行。

总之,ES6中的generator函数只能暂停自己,外部代码只能控制它恢复执行,我们稍后对原理进行说明。

所以,基本上一个generator函数可以根据需要多次暂停并恢复执行。如果在这样的函数里书写死循环,例如while(true),编程常识中是比较忌讳的,可对于generator函数来说,死循环是一个绝对正确的用法。


Syntax Please!(语法讲解)

我们来看看generator函数的语法,首先是函数定义:
function *foo() {
    // ..
}
我们注意到定义中有个 * 符号,有点像其它的语言,像是指针,但其实只是用来定义generator函数用的。

function* foo(){}和function *foo(){}都是正确的写法,作者更喜欢后者。

其实generator函数和普通函数差不多,只是多了一两个新语法。

新的语法是yield关键字,用法是【yield js表达式】,当恢复运行时,【yield js表达式】整体就变成由外部传递进来的值。

例子:
function *foo() {
    var x = 1 + (yield "foo");
    console.log(x);
}
yield "foo"表示暂停执行,并向外部传递“foo”字符串,当外部恢复foo函数的执行时,yield "foo"整体变成了由外部传递进来的值,加上1赋值给x变量。

能够看到generator函数是如何与外部进行交流的,自暂停以后,过一阵子又恢复执行,就好像yield是一种请求函数一样,在请求一个值。

当然也可是直接使用yield而不加js表达式,下面是例子

// note: `foo(..)` here is NOT a generator!!
function foo(x) {
    console.log("x: " + x);
}

function *bar() {
    yield; // just pause
    foo( yield ); // pause waiting for a parameter to pass into `foo(..)`
}

Generator Iterator(循环特性,恢复执行的用法)

通过调用next()可以一步步迭代数据,就像迭代一个数组,例如[1,2,3,4,5],第一次运行next()得到1,第
var message = it.next();

二次运行next()得到2,一直取出所有数字后,再运行next()会得到null或者false或者其他值,表示已经迭代了所有数据。

控制generator函数的方法正是建立迭代器,并调用迭代器,听起来好像有点复杂,先看看例子:

function *foo() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
}
首先我们要构造一个迭代器,怎么做呢?很简单:
var it = foo();
像普通函数一样调用generator函数,是不会运行任何代码的。(译者:而是构造了迭代器)

这和普通函数构造实例的格式不太一样,为什么不是var it = new foo()这样构造呢?这背后的原理十分复杂,超出了本篇文章的范围。

至少通过构造迭代器,就可能进行迭代了:
var message = it.next();
我们会对应yield 1语句而得到1数字,但是返回的数据结构并不止是数字1。
console.log(message); // { value:1, done:false }
通过调用next(),yield 1会返回value:1,同时还会返回done:false,表示迭代还没完成。

我们继续迭代:

console.log( it.next() ); // { value:2, done:false }
console.log( it.next() ); // { value:3, done:false }
console.log( it.next() ); // { value:4, done:false }
console.log( it.next() ); // { value:5, done:false }
有意思的是循环了所有数据,但是done的值还是false,表示迭代还未完成,只有再进行一次迭代

console.log( it.next() ); // { value:undefined, done:true }
这时才表示迭代完成,但已经没有对应的yield返回给value的数据。

如果使用return函数,是否可以返回数据给value呢?

答案是可以的

function *foo() {
    yield 1;
    return 2;
}

var it = foo();

console.log( it.next() ); // { value:1, done:false }
console.log( it.next() ); // { value:2, done:true }
但是最好不要这样做

因为如果使用了for..of语法,最后的return数据会丢失掉。

再继续看一下generator函数,在迭代中是如何接收参数和发送参数的:


function *foo(x) {
    var y = 2 * (yield (x + 1));
    var z = yield (y / 3);
    return (x + y + z);
}

var it = foo( 5 );

// note: not sending anything into `next()` here
console.log( it.next() );       // { value:6, done:false }
console.log( it.next( 12 ) );   // { value:8, done:false }
console.log( it.next( 13 ) );   // { value:42, done:true }
我们可以看到,初始化时,还是需要向形式参数传值(示例中的x变量),这一点和普通函数一样。

第一次调用next()时,没有向里面传值,因为没有运行过的yield去接收数据。

但是如果非要向里面传值,那么什么也不会发生,generator函数会抛弃这个值(但有的浏览器可能会报错)

不难看出,第一个yield(x+1)会向外返回数字6,第二次调用next(12),会将12数字传递到等待状态的yield(x+1)语句,所以yield(x+1)就变成了数字12,var y = 2 * 12; ,y的值就变为了24。然后接下来运行yield(y/3)相当于yield(24/3),就意味着向外返回数字8,第三次调用next(13)会将13传递给等待状态的yield(y/3)表达式,使z被赋值为13。

最后一步 return (x + y +z)就是 return(5 + 24 +13),42将会是最终的返回值。

如果没弄懂就再多读一读,通常第一次接触会感到困惑。

下面说一下for...of

ES6有语法直接支持迭代集合,那就for...of循环,一直将集合循环完为止。

示例:
function *foo() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
    return 6;
}

for (var v of foo()) {
    console.log( v );
}
// 1 2 3 4 5

console.log( v ); // still `5`, not `6` :(
可以看到迭代器(for...of)直接捕捉到了foo()作为集合,并开始循环,直到出现done:true为止,但不会捕获return中的数据。

另外for...of中没有显示next()语法进行调用,所有循环期间也不能进行传参。

Summary总结

目前这些仅仅是generator函数的表面特性,围绕它还有很多内容:

1.如何进行异常捕获?
2.能不能在generator函数中调用另一个generator函数?
3.异步代码如何结合generator函数进行编程?

下篇文章见,这是第一篇,共四篇
(译者注:倒数两篇文章,原作者着重推销他的多线程开源代码ASQ,@-@)


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值