ES6 Generators函数基础知识/概念讲解,入门教程

Javascript ES6最令人振奋人心的众多新特性之一是一个新品种函数,它叫作generator。这个名字有一点怪异,但是行为看起来要更怪异一些。本文旨在解释它的基本工作原理,同时让你逐渐理解对于未来JS它们为何如此强大!

define('CONCATENATE_SCRIPTS', false);

Run-To-Completion(运行到结束)

我们通常对于传统函数(function)的预期就是从“运行到结束”,那么首先让我们来观察一下,generators与传统函数的差异。

无论你有没有意识到,你已经对function建立了相当牢固的基础认知,那就是:一旦一个函数(function)开始运行,它必须运行结束,其它的JS代码才能继续运行。

栗子🌰:

setTimeout(function () {
    console.log("Hello World");
}, 1);

function foo() {
    // 提示:没事不要像这样作死
    for (var i = 0; i <= 1E10; i++) {
        console.log(i);
    }
}

foo();
// 0..1E10
// "Hello World"

看这里,for循环会运行相当长一段时间才能结束,至少超过1毫秒以上,但是我们用来“Hello World”的计时器回调函数却不能打断foo()函数的运行,所以它只能安静的等待foo()函数运行结束才能轮到它执行。

如果 foo() 函数的运行可以被打断,感觉会不会很刺激? 这不会导致一场浩劫?

对于多线程变成来说,这毫无以为将是一场噩梦挑战,但是万分幸运的是在JS战场上这不是问题,因为JS向来是单线程(任何时间都只有一个命令/函数在执行)。

注意:Web Workers是一种多线程机制,它可以让JS运行在完全独立的子线程中,并且与主线程并行运算。但是这不会引发多线程并发症,原因是两个线程只能通过正常的异步事件进行通信,它是遵循事件循环规则的,就像“run-to-completion”要求的那样,一次执行一个命令/函数。

Run..Stop..Run(走走..停停..走走)

伴随ES6 generators,我们有了不同的函数类型,它可以在运行过程中暂停,可能暂停一次或者多次,稍后还可以恢复运行,其它的代码则可以在它暂停时运行。

如果你曾经听说过并发或线程编程,你也许会见过这个术语——“cooperative”(可合作的),简单的理解就是一个进程(本文指函数)自己选择何时将接受一个打断操作,那么它将可以和其它代码进行合作。

ES6 generator函数在它的并发行为中是“cooperative”(可合作的)。你可以在generator函数内部使用新关键字“yield”暂停函数运行,任何方法都不可以在外部暂停一个generator的运行;当它遇到一个“yield”时将暂停自己。

但是,一旦一个generator通过“yield”暂停住自己,它没办法自己恢复自己的运行,必须通过一个外在的操作恢复generator的运行。接下来我们将看到这一切是如何发生的。

所以,简单来看,一个generator可以多次停止/恢复运行,次数有你来决定。事实上,你可以让一个generator进行无限循环(就像臭名昭著的 while (true) {..} ),那么它将永远不会停止。虽然这通常是一个疯狂的操作或者一个错误,然而一个完全清醒的generator函数有时可能正是你需要的!

更重要的是,对于一个generator函数来说,伴随着它的运行,暂停/恢复命令不仅仅是一个操作,它同时可以进行双向数据传输,将数据传入/传出generator函数。对于一个正常的函数,你在开始执行时获取到参数,然后在运行结束时return一个结果值。但是对于generator函数,你可以通过“yeild”多次把信息传出,同时可以通过恢复命令再把信息传入。

翠花,上语法!

让我们深入体会一下这些新语法和扣人心弦的generator函数。

首先,新的声明语法:

function *foo() {
    // ...
}

看到那个*了吗?看起来既陌生又新颖。对于其他的语言,它看起来就像那恐怖的指针。但是不要慌张!这就仅仅是一个标记,告诉你这是一个generator函数。

你可能在其它文章或者文档中看到有这样使用的“function* foo() {}”,而不是“function *foo () {}”,区别在于*旁的空格。这两个写法都可以,但是我决定使用“function *foo () {}”,因为我觉得这样更准确一些,所以文中我都是这样使用的。

现在,我们来谈谈generator函数的内容如何写。Generator函数基本上就是正常的JS函数。在generator函数内部只有很少量的语法需要学习。

我们主要需要打交道的新东东就是上面提到的关键字“yield”。yield__ 被称为“yield 表达式”(而并不是一个声明),因为当我们恢复generator时,我们将向内部回传值,而传回的值就是 yield__ 表达式的结果。

栗子🌰:

function *foo() {
    var x = 1 + (yield "foo");
    console.log(x);
}

当执行到yield “foo”表达式时generator函数将暂停,同时把“foo”字符串值传出,无论generator何时恢复,恢复时传回的值将作为yield “foo”表达式的结果执行+1的运算然后赋值给变量x。

看到双向通信了吗?你发送值“foo”出去,暂停自己,稍后的某个时刻(可能是瞬间、可能是很久)generator将被恢复同时获得一个返回值。这就好像是yield执行了一个请求来获取返回值一样。

在一些情况下,你可以在表达式/声明中只使用yield来进行暂停,相当于一个假象的的undefined值被传出。🌰:

// note: foo(..) 不是一个 generator!!
function foo(x) {
    console.log("x: " + x);
}

function *bar() {
    yield; // 只是暂停
    foo( yield ); // 暂停并等待一个参数被传入foo函数
}

Generator Iterator(generator迭代器)

Iterator是一个特殊的行为类型,实际上是一种设计模式,其中我们可以通过调用next()依次步入一个有序值序列的每一个值。想象一下在一个含有5个值得数组([1, 2, 3, 4, 5])上使用迭代器的场景。第一个next()将返回1,第二个next()将返回2,以此类推。当所有值都被返回之后,next()将返回 null 或者 false 或者 其它的信号通知你已经迭代了数组中的所有值。

我们用来从外部控制generator函数的方式是去构造一个generator iterator并和它交互。这听起来就像看起来一样难懂。看看这无聊的🌰吧:

function *foo() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
}

为了步入 *foo() generator函数的所有值,我们需要构造一个iterator(迭代器)。怎么搞?很简单!

 var it = foo();

什么!我们像正常调用函数一样调用generator函数,实际上并没有执行它的内部代码。

这可能有一点让你头大。你也可能很想知道,为什么不是 var it = new foo()。额,这原因很复杂,也不在我们讨论的范畴。。。

所以现在,想要去迭代我们的generator函数,只需这样做:

var message = it.next();

这将通过yield 1向我们返回 1,但是这并不是我们获取的唯一值。

console.log(message); // { value: 1, done: false }

我们实际上通过next()获取到的是一个object(对象),它包含一个 value 属性来承载 yield 的输出,一个布尔值 done 属性标明是否generator函数已经运行结束。

让我们继续运行我们的迭代器:

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 }

搞笑的是,当我们获取到数值 5 的时候,done 属性依然是 false。这是因为在技术上,generator函数并没有运行结束。我们仍然需要去执行最后一次 next() 指令,同时如果我们传入一个值,它将最为yield 5表达式的返回值。只有这样generator函数才完全执行完。

所以,现在:

console.log( it.next() ); // { value: undefined, done: true }

所以,最后的返回值表明我们的generator函数完全执行结束,但是结果里并没有返回值(因为我们已经消耗掉所有的 yield__ )。

你这时可能会好奇,能否在一个generator函数中使用 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 }

也不能。。。

在generator函数中依赖return返回值可能并不是一个好方法,因为当通过 for..of (下面会讲到)的方式迭代generator函数时,最后一个 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: 这里并没有向next()传入任何数据
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 ),就像平时使用传统函数时那样,赋值 x 等于5;

对于第一个 next(..) ,我们不传入任何值。为何?因为目前尚无 yield 表达式接收传值。

但是如果我们执意向第一个 next(..) 传入参数,什么都不会发生。它只是一个被忽略的参数。ES6表示在这个示例中generator函数会忽略这个未被使用的参数。(注意:这样写会有兼容性问题,请具体测试)。

yield(x + 1) 将传出数值 6。第二个next(12) 调用会向等待中的 yield(x + 1) 表达式传入值 12,所以 y = 12 * 2 = 24。接下来随后的 yield(y / 3) 相当于 yield(24 / 3)将向外部传出值 8 ,第三个 next(13) 向等待中的 yield(y / 3)表达式传入值 13,使 z = 13。

最后,返回  x + y + z = 5 + 24 + 13 = 42,42也被当做generator函数的最后一个值被返回。

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 ); // 仍然是 5,而不是 6。尼玛

如你所见,由foo()创建的迭代器会自动被 foo..of 循环捕获并进行迭代,一次迭代获取一个值,直到 done: true 出现。只要 done 仍然等于 false,value属性值便会被提取并赋值给迭代变量(这里指 v)。一旦 done 等于 true,循环迭代将结束(同时不会对最后的返回值 value 最任何处理,如果有的话)。

如上所述,你可以发现 for..of 循环会忽略并遗弃返回值6。同样,因为没有一个显式调用 next(),for..of循环在这种情况下将不能使用。

概述

好吧,这就是它的基础知识。如果仍然有一丝头大也不要担忧。大家第一次都是这样。

很自然你会惊奇这个新东东能为的编程做些什么。当然,对于它我们还有更多需要了解的。我们还只是撕开了封面而已。所以我们必须潜入深处去发现它的强大。

原文:https://davidwalsh.name/es6-generators

很少进行这么大片文章的翻译工作,这篇文章还算通熟易懂,所以便尝试翻译了一下,有些地方理解了但是可能描述的不准确,有些地方可能理解的就错了,还请留言指正,我会看的,也会随时修正,感谢!