前言
今天讨论的新特性让我非常兴奋,因为这个特性是 ES6 中最神奇的特性。
这里的“神奇”意味着什么呢?对于初学者来说,该特性与以往的 JS 完全不同,甚至有些晦涩难懂。从某种意义上说,它完全改变了这门语言的通常行为,这不是“神奇”是什么呢。
不仅如此,该特性还可以简化程序代码,将复杂的“回调堆栈”改成线性执行的形式。
简介
- 通常的函数以
function
创建生成器,但 Generator 函数以function*
创建。 - 在 Generator 函数内部,
yield
是一个关键字,和return
有点像。不同点在于,所有函数(包括 Generator 函数)都只能返回一次,而在 Generator 函数中可以 yield 任意次。yield 表达式暂停了 Generator 函数的执行,然后可以从暂停的地方恢复执行。
常见的函数不能暂停执行,而 Generator 函数可以,这就是这两者最大的区别。
Iterator
迭代器(Iterator
)表示可以被遍历迭代的对象,以编程的方式返回集合中的下一项,迭代器对象都拥有next()
方法,返回{value:'',done:bool}
,value
表示当前迭代位置的返回值,done
表示是否迭代结束,结束时其值为false
。
迭代对象之所以可被迭代是因为其有Symbol(Symbol.iterator)
属性。如果有什么疑问可以参考上一章。
Generator
生成器(Generator
)是一个返回迭代器对象(Iterator
)的函数
- 使用
function* funName(){}
创建 - 使用声称其表达式创建生成器:
let iterator = function* (){}
- 函数体中使用
yield
关键字定义每次迭代器调用next()
返回的value
结果。yield
指令相当于起到暂停代码执行的作用。只有在迭代器调用next()
时,才会出发下一个yield
继续执行 - 迭代完
yield
后,下一次迭代会返回return
的值。如果return
在中间,则迭代到return
行后直接跳出。
1 | function *createIterator(){ |
原理
当我们调用一个Generator
函数时并没有立即执行,而是返回了一个generator
对象,也就是上边的iterator
。此时函数就立即暂停在函数代码的第一个yield
处。
当我们每次调用Generator
对象的.next()
方法时,函数就开始执行,直至遇到下一个yield
表达式为止。
所以我们每次调用iterator.next()
时都会得到一个不同的字符串,这些字符串都是在函数内部通过yield
表达式产生的值。
从技术层面上讲,每当Generator
函数执行遇到yield
表达式时,函数的栈帧 – 本地变量,函数参数,临时值和当前执行的位置,就从堆栈移除,但是Generator
对象保留了对该栈帧的引用,所以下次调用.next()
方法时,就可以恢复并继续执行。
值得提醒的是Generator
并不是多线程。在支持多线程的语言中,同一时间可以执行多段代码,并伴随着执行资源的竞争,执行结果的不确定性和较好的性能。而Generator
函数并不是这样,当一个Generator
函数执行时,它与其调用者都在同一线程中执行,每次执行顺序都是确定的,有序的,并且执行顺序不会发生改变。与线程不同,Generator
函数可以在内部的yield
的标志点暂停执行。
迭代器 Iterator
通过上篇文章,我们知道迭代器并不是 ES6 的一个内置的类,而只是作为语言的一个扩展点,你可以通过实现 [Symbol.iterator]()
和 .next()
方法来定义一个迭代器。
但是,实现一个接口还是需要写一些代码的,下面我们来看看在实际中如何实现一个迭代器,以实现一个 range
迭代器为例,该迭代器只是简单地从一个数累加到另一个数,有点像 C 语言中的 for (;;)
循环。
1 | // This should "ding" three times |
现在有一个解决方案,就是使用 ES6 的类。(如果你对 class
语法还不熟悉,不要紧,我会在将来的文章中介绍。)
1 | class RangeIterator { |
这种实现方式与 Java 和 Swift 的实现方式类似,看上去还不错,但还不能说上面代码就完全正确,代码没有任何 Bug?这很难说。我们看不到任何传统的 for (;;)
循环代码:迭代器的协议迫使我们将循环拆散了。
不过我们如果通过Generator
函数实现迭代器那么就会容易很多
1 | function* range(start, stop) { |
上面这 4 行代码就可以完全替代之前的那个 23 行的实现,替换掉整个 RangeIterator
类,这是因为 Generator 天生就是迭代器,所有的 Generator 都原生实现了 .next()
和 [Symbol.iterator]()
方法。你只需要实现其中的循环逻辑就够了。
我们可以使用作为迭代器的 Generator 的哪些功能呢?
- 使任何对象可遍历 – 编写一个 Genetator 函数去遍历
this
,每遍历到一个值就 yield 一下,然后将该 Generator 函数作为要遍历的对象上的[Symbol.iterator]
方法的实现。 - 简化返回数组的函数 – 假如有一个每次调用时都返回一个数组的函数,比如:
1 | // Divide the one-dimensional array 'icons' |
使用Generator
可以简化这类函数
1 | function* splitIntoRows(icons, rowLength) { |
这两者唯一的区别在于,前者在调用时计算出了所有结果并用一个数组返回,后者返回的是一个迭代器,结果是在需要的时候才进行计算,然后一个一个地返回。
- 无穷大的结果集 – 我们不能构建一个无穷大的数组,但是我们可以返回一个生成无尽序列的 Generator,并且每个调用者都可以从中获取到任意多个需要的值。
- 重构复杂的循环 – 你是否想将一个复杂冗长的函数重构为两个简单的函数?Generator 是你重构工具箱中一把新的瑞士军刀。对于一个复杂的循环,我们可以将生成数据集那部分代码重构为一个 Generator 函数,然后用
for-of
遍历:for (var data of myNewGenerator(args))
。 - 构建迭代器的工具 – ES6 并没有提供一个可扩展的库,来对数据集进行
filter
和map
等操作,但 Generator 可以用几行代码就实现这类功能。
例如,假设你需要在Nodelist
上实现与 Array.prototype.filter
同样的功能的方法。小菜一碟的事:
1 | function* filter(test, iterable) { |
所以,Generator 很实用吧?当然,这是实现自定义迭代器最简单直接的方式,并且,在 ES6 中,迭代器是数据集和循环的新标准。
但,这还不是 Generator 的全部功能。
生成器委托 yield*
1 | function* g1() { |
这玩意儿其实也很好理解,带*的函数是我们的generator
语句,而yield
开头的则是我们的“产出”语句。二者相结合可以理解为执行至yield
时继续创建一个generator
,emmmm。。。很容易理解吧~
没理解我暂时也没想到有什么解释的方法(小声bb。。。)
分段式代码
当我们需要手动控制异步进程时,可以通过将Promise
和Generator
结合起来实现
1 | // 异步ajax代码 |
兼容性
在服务器端,现在就可以直接在 io.js 中使用 Generator
(或者在 NodeJs 中以 --harmony
启动参数来启动 Node)。
在浏览器端,目前只有 Firefox 27 和 Chrome 39 以上的版本才支持 Generator,如果想直接在 Web 上使用,你可以使用 Babel 或 Google 的 Traceur 将 ES6 代码转换为 Web 友好的 ES5 代码。
一些题外话:JS 版本的 Generator 最早是由Brendan Eich
实现,他借鉴了 Python Generator 的实现,该实现的灵感来自 Icon,早在 2006 年的 Firefox 2.0 就吸纳了 Generator。但标准化的道路是坎坷的,一路下来,其语法和行为都发生了很多改变,Firefox 和 Chrome 中的 ES6 Generator 是由 Andy Wingo 实现 ,这项工作是由 Bloomberg 赞助的。
实现
1 | // cb 也就是编译过的 test 函数 |