ES6生成器的基础知识
es6生成器的基础知识
es6生成器的基础知识
es6生成器的基础知识运行到完成(run-to-completion)运行...停止...运行(run..stop..run)语法(syntax please!)生成器 迭代器(generator iterator)总结es6生成器的基础知识
javascript es6中最令人兴奋的新特性之一是新增了一种新类型的函数,称为generator(下面会被翻译为“生成器”)。这个名字有点奇怪,但乍一看,这个函数的行为会更奇怪。本文旨在解释它们如何工作的基础知识,并帮助你理解为什么它可以让js变得更加强大。
运行到完成(run-to-completion)
在我们讨论生成器时要注意的第一件事是,在“运行到完成”期望方面,它们与普通函数有何不同。
不管你有没有意识到,你总是可以对你运行的函数做出假设:一旦一个函数开始执行,在任何其他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循环语句将花费相当长的时间才能执行完成,时间超过一毫秒,但是带有console.log(..)语句的定时器的回调函数在运行时不能中断foo()函数的执行,所以它被卡在了后面(事件循环),耐心地等待轮到它。
但是,如果foo()函数的执行可以被中断呢这不会对我们的程序造成破坏呢
这正是多线程的噩梦挑战,但在javascript领域,我们很幸运不必担心这些事情,因为js总是单线程的(任何时候只执行一个命令/函数)。
注意:web workers是一种机制,在这种机制中,你可以将一个完整的单独的线程转移到js程序的一部分中运行,完全与你的主js程序线程并行。这不会在我们的程序中引入多线程复杂性的原因是,两个线程只能通过正常的异步事件相互通信,而异步事件总是遵循时间循环,一次执行一个的行为。
运行…停止…运行(run…stop…run)
对于es6生成器,提供给我们了一种不同的函数,它可以在中间暂停,一次或多次,然后恢复,允许其他代码在这些暂停期间运行。
如果你曾经读过关于并发性或线程编程的内容,那么你可能听说过“协作”,它和进程有些类似(在我们的例子中是一个函数)自己选择何时允许中断,以便它能够与其他代码合作。这个概念与“抢占式”(preemptive)形成对比,抢占式(preemptive)表示进程/函数可以违背其意愿被中断。
es6生成器函数的并发行为是“协作的”。在生成器函数体中,使用new yield关键字从函数内部暂停。任何事件都不能让生成器函数停止执行;当遇到yield关键字,它会停下来。
然而,一旦生成器函数被yield之后,它就不能自己恢复了。必须使用外部控制来重新启动生成器。我们稍后会解释这是如何发生的。
所以,基本上,一个生成器函数可以停止并重新启动,只要你愿意。实际上,你可以指定一个带无限循环的生成器函数(比如臭名昭著的while (true) {..})那基本上永远不会结束。
虽然这在一个普通的js程序中通常是疯狂的或错误的,但是有了生成器函数,它是完全正常的,有时正是你想要做的!
更重要的是,随着它的发展,这种停止和启动不仅仅是对生成器函数执行的控制,而且它还支持生成器函数的内外双向消息通信。对于普通函数,在函数开头获取参数,在结尾得到返回值。使用生成器函数,你可以使用每个yield发送消息,在每次重新启动时返回消息。
语法(syntax please!)
下面开始深入了解下生成器函数的语法。
先看语法声明:
function *foo() { // .. }
注意到*了吗着看起来有点奇怪。对于那些接触过其他编程语言的人来说,它看起来很像一个函数返回值指针。但这里不要混淆!这只是一种定义生成器函数类型的方法。
您可能已经看到过其他使用function* foo(){}而不是function *foo(){}(*位置不同)的文章/文档。这两种方法都是有效的,但我认为function *foo(){}()更准确,所以我在这里使用的就是这种方法。
现在,我们来讨论一下生成器函数的内容。生成器函数在大多数方面都只是普通的js函数。在生成器函数中几乎没有需要学习的新语法。
正如上面提到的,我们主要用到的是yield关键字。yield关键字后面跟着的语句叫“yield 表达式”(不是一个声明)因为当我们重新启动生成器时,我们将返回一个值,无论我们发送什么,都将是yield 表达式的计算结果。
例子:
function *foo() { var x = 1 + (yield "foo"); console.log(x); }
yield "fool表达式会返回一个"fool"字符串,在生成器函数暂停的时候,在生成器函数重启之后,表达式的返回值加上1会被赋值给变量x。
看到双向沟通了吗你将值“foo”发送出去,自己暂停,然后在稍后的某个时间点(可能是立即的,也可能是很久以后!),生成器将重新启动并返回值。似乎yield关键字是对某个值的请求。
在表达式的任何位置,你都可以在表达式/语句中单独使用yield,并且输出了一个假定的未定义值。所以:
// 注意: `foo(..)` 这不是一个生成器函数!! function foo(x) { console.log("x: " + x); } function *bar() { yield; // 暂停 foo( yield ); // 暂停等待一个参数传入 `foo(..)` }
生成器 迭代器(generator iterator)
“生成器 迭代器”。很拗口,对吧
迭代器是一种特殊的行为,实际上是一种设计模式,我们通过调用next()一步一步地遍历一组有序的值。例如,在数组中使用迭代器,数组中有5个值:[1,2,3,4,5]。第一个next()调用将返回1,第二个next()调用将返回2,依此类推。在返回所有值之后,next()将返回null或false或其他值,告诉你已经遍历了数组中的所有值。
我们从外部控制生成器函数的方法是构造并与生成器迭代器交互。这听起来比实际要复杂得多。看下面的例子:
function *foo() { yield 1; yield 2; yield 3; yield 4; yield 5; }
要逐步遍历*foo()生成器函数的值,我们需要构造一个迭代器。我们怎么做呢简单!
var it = foo();
现在,要开始迭代生成器函数,我们只需要:
var message = it.next();
yield 语句会返回1,但这不是我们得到的唯一东西。
console.log(message); // { value:1, done:false }
实际上,我们从每个next()调用返回一个对象,该对象具有value的值属性,done是一个布尔值,指示生成器函数是否完全完成。
让我们继续我们的迭代:
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。这是因为在技术上,生成器函数并不完整。我们仍然需要调用next()调用,如果我们发送一个值,它必须作为yield 5表达式的结果设置。只有这样才能完成生成器的功能。
因此:
console.log( it.next() ); // { value:undefined, done:true }
因此,我们的生成器函数的最终结果是完成了函数,但是没有给出结果(因为所有的yield语句已经被执行完了)。
此时您可能会想,我是否可以在生成器函数的使用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返回值将被丢弃。
为了完整起见,我们还将查看在迭代生成器函数时同时向生成器函数发送消息:
function *foo(x) { var y = 2 * (yield (x + 1)); var z = yield (y / 3); return (x + y + z); } var it = foo( 5 ); 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 }
可以看到,我们仍然可以通过初始foo(5)迭代器实例化调用传入参数(x),就像普通函数一样,使x值为5。
第一个next()中我们没有传入参数,因为没有yield表达式来接收我们传递的东西。
但是,如果我们向第一个next()调用传递了一个参数,不会发生任何事情,它只是一个多余的值。es6表示生成器函数在本例中忽略未使用的值。(注意:在写这篇文章的时候,chrome和ff都很好,但是其他可能还没有完全兼容,在这种情况下可能会错误地抛出错误)。
yield (x+1)将返回6,第二个next(12)调用的时候会将12传给等待的yield (x + 1)表达式,因此y值被设置为12 * 2,值为24,随后yield (y / 3) (yield (24 / 3))的返回值是8。接下来的第三个next(13)调用向等待的yield (y / 3)表达式传值13,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`
可以看到,foo()创建的迭代器是由for..of自动捕获的。for..of循环会为你自动迭代,直到一个done:true出现。只要done为false,它就会自动提取value属性并将其赋给你的迭代变量(在本例中为v)。一旦done值为true,循环迭代就停止(如果返回任何最终值,则不执行任何操作)。
如上所述,您可以看到for..of循环忽略并丢弃返回值6。另外,由于没有暴露的next()调用,for..of中没办法向上面那样传入值。