生成值
在前一节中,我们提到了一个generator的有趣用法,作为一种生产值的方式。这 不是 我们本章主要关注的,但如果我们不在这里讲一下基本我们会想念它的,特别是因为这种用法实质上是它的名称的由来:生成器。
我们将要稍稍深入一下 迭代器 的话题,但我们会绕回到它们如何与generator关联,并使用generator来 生成 值。
发生器与迭代器
想象你正在生产一系列的值,它们中的每一个都与前一个值有可定义的关系。为此,你将需要一个有状态的发生器来记住上一个给出的值。
你可以用函数闭包(参加本系列的 作用域与闭包)来直接地实现这样的东西:
var gimmeSomething = (function(){
var nextVal;
return function(){
if (nextVal === undefined) {
nextVal = 1;
}
else {
nextVal = (3 * nextVal) + 6;
}
return nextVal;
};
})();
gimmeSomething(); // 1
gimmeSomething(); // 9
gimmeSomething(); // 33
gimmeSomething(); // 105
注意: 这里nextVal
的计算逻辑已经被简化了,但从概念上讲,直到 下一次 gimmeSomething()
调用发生之前,我们不想计算 下一个值(也就是nextVal
),因为一般对于持久性更强的,或者比简单的number
更有限的资源的发生器来说,那可能是一种资源泄漏的设计。
生成随意的数字序列不是是一个很真实的例子。但是如果你从一个数据源中生成记录呢?你可以想象很多相同的代码。
事实上,这种任务是一种非常常见的设计模式,通常用迭代器解决。一个 迭代器 是一个明确定义的接口,用来逐个通过一系列从发生器得到的值。迭代器的JS接口,和大多数语言一样,是在你每次想从发生器中得到下一个值时调用的next()
。
我们可以为我们的数字序列发生器实现标准的 迭代器;
var something = (function(){
var nextVal;
return {
// `for..of`循环需要这个
[Symbol.iterator]: function(){ return this; },
// 标准的迭代器接口方法
next: function(){
if (nextVal === undefined) {
nextVal = 1;
}
else {
nextVal = (3 * nextVal) + 6;
}
return { done:false, value:nextVal };
}
};
})();
something.next().value; // 1
something.next().value; // 9
something.next().value; // 33
something.next().value; // 105
注意: 我们将在“Iterables”一节中讲解为什么我们在这个代码段中需要[Symbol.iterator]: ..
这一部分。在语法上讲,两个ES6特性在发挥作用。首先,[ .. ]
语法称为一个 计算型属性名(参见本系列的 this与对象原型)。它是一种字面对象定义方法,用来指定一个表达式并使用这个表达式的结果作为属性名。另一个,Symbol.iterator
是ES6预定义的特殊Symbol
值。
next()
调用返回一个对象,它带有两个属性:done
是一个boolean
值表示 迭代器 的完成状态;value
持有迭代的值。
ES6还增加了for..of
循环,它意味着一个标准的 迭代器 可以使用原生的循环语法来自动地被消费:
for (var v of something) {
console.log( v );
// 不要让循环永无休止!
if (v > 500) {
break;
}
}
// 1 9 33 105 321 969
注意: 因为我们的something
迭代器总是返回done:false
,这个for..of
循环将会永远运行,这就是为什么我们条件性地放进一个break
。对于迭代器来说永不终结是完全没有问题的,但是也有一些情况 迭代器 将运行在有限的值的集合上,而最终返回done:true
。
for..of
循环为每一次迭代自动调用next()
——他不会给next()
传入任何值——而且他将会在收到一个done:true
时自动终结。这对于在一个集合的数据中进行循环十分方便。
当然,你可以手动循环一个迭代器,调用next()
并检查done:true
条件来知道什么时候停止:
for (
var ret;
(ret = something.next()) && !ret.done;
) {
console.log( ret.value );
// 不要让循环永无休止!
if (ret.value > 500) {
break;
}
}
// 1 9 33 105 321 969
注意: 这种手动的for
方式当然要比ES6的for..of
循环语法难看,但它的好处是它提供给你一个机会,在有必要时传值给next(..)
调用。
除了制造你自己的 迭代器 之外,许多JS中(就ES6来说)内建的数据结构,比如array
,也有默认的 迭代器:
var a = [1,3,5,7,9];
for (var v of a) {
console.log( v );
}
// 1 3 5 7 9
for..of
循环向a
要来它的迭代器,并自动使用它迭代a
的值。
注意: 看起来像是一个ES6的奇怪省略,普通的object
有意地不带有像array
那样的默认 迭代器。原因比我们要在这里讲的深刻得多。如果你想要的只是迭代一个对象的属性(不特别保证顺序),Object.keys(..)
返回一个array
,它可以像for (var k of Object.keys(obj)) { ..
这样使用。像这样用for..of
循环一个对象上的键,与用for..in
循环内很相似,除了在for..in
中会包含[[Prototype]]
链的属性,而Object.keys(..)
不会(参见本系列的 this与对象原型)。
Iterables
在我们运行的例子中的something
对象被称为一个 迭代器,因为它的接口中有next()
方法。但一个紧密关联的术语是 iterable,它指 包含有 一个可以迭代它所有值的迭代器的对象。
在ES6中,从一个 iterable 中取得一个 迭代器 的方法是,iterable 上必须有一个函数,它的名称是特殊的ES6符号值Symbol.iterator
。当这个函数被调用时,它就会返回一个 迭代器。虽然不是必须的,但一般来说每次调用应当返回一个全新的 迭代器。
前一个代码段的a
就是一个 iterable。for..of
循环自动地调用它的Symbol.iterator
函数来构建一个 迭代器。我们当然可以手动地调用这个函数,然后使用它返回的 iterator:
var a = [1,3,5,7,9];
var it = a[Symbol.iterator]();
it.next().value; // 1
it.next().value; // 3
it.next().value; // 5
..
在前面定义something
的代码段中,你可能已经注意到了这一行:
[Symbol.iterator]: function(){ return this; }
这段有点让人困惑的代码制造了something
值——something
迭代器 的接口——也是一个 iterable;现在它既是一个 iterable 也是一个 迭代器。然后,我们把something
传递给for..of
循环:
for (var v of something) {
..
}
for..of
循环期待something
是一个 iterable,所以它会寻找并调用它的Symbol.iterator
函数。我们将这个函数定义为简单地return this
,所以它将自己给出,而for..of
不会知道这些。
Generator迭代器
带着 迭代器 的背景知识,让我们把注意力移回generator。一个generator可以被看做一个值的发生器,我们通过一个 迭代器 接口的next()
调用每次从中抽取一个值。
所以,一个generator本身在技术上讲并不是一个 iterable,虽然很相似——当你执行generator时,你就得到一个 迭代器:
function *foo(){ .. }
var it = foo();
我们可以用generator实现早前的something
无限数字序列发生器,就像这样:
function *something() {
var nextVal;
while (true) {
if (nextVal === undefined) {
nextVal = 1;
}
else {
nextVal = (3 * nextVal) + 6;
}
yield nextVal;
}
}
注意: 在一个真实的JS程序中含有一个while..true
循环通常是一件非常不好的事情,至少如果它没有一个break
或return
语句,那么它就很可能永远运行,并同步地,阻塞/锁定浏览器UI。然而,在generator中,如果这样的循环含有一个yield
,那它就是完全没有问题的,因为generator将在每次迭代后暂停,yield
回主程序和/或事件轮询队列。说的明白点儿,“generator把while..true
带回到JS编程中了!”
这变得相当干净和简单点儿了,对吧?因为generator会暂停在每个yield
,*something()
函数的状态(作用域)被保持着,这意味着没有必要用闭包的模板代码来跨调用保留变量的状态了。
不仅是更简单的代码——我们不必自己制造 迭代器 接口了——它实际上是更合理的代码,因为它更清晰地表达了意图。比如,while..true
循环告诉我们这个generator将要永远运行——只要我们一直向它请求,它就一直 产生 值。
现在我们可以在for..of
循环中使用新得发亮的*something()
generator了,而且你会看到它工作起来基本一模一样:
for (var v of something()) {
console.log( v );
// 不要让循环永无休止!
if (v > 500) {
break;
}
}
// 1 9 33 105 321 969
不要跳过for (var v of something()) ..
!我们不仅仅像之前的例子那样将something
作为一个值引用了,而是调用*something()
generator来得到它的 迭代器,并交给for..of
使用。
如果你仔细观察,在这个generator和循环的互动中,你可能会有两个疑问:
- 为什么我们不能说
for (var v of something) ..
?因为这个something
是一个generator,而不是一个 iterable。我们不得不调用something()
来构建一个发生器给for..of
,以便它可以迭代。 something()
调用创建一个 迭代器,但是for..of
想要一个 iterable,对吧?对,generator的 迭代器 上也有一个Symbol.iterator
函数,这个函数基本上就是return this
,就像我们刚才定义的something
iterable。换句话说generator的 迭代器 也是一个 iterable!
停止Generator
在前一个例子中,看起来在循环的break
被调用后,*something()
generator的 迭代器 实例基本上被留在了一个永远挂起的状态。
但是这里有一个隐藏的行为为你处理这件事。for..of
循环的“异常完成”(“提前终结”等等)——一般是由break
,return
,或未捕捉的异常导致的——会向generator的 迭代器 发送一个信号,以使它终结。
注意: 技术上讲,for..of
循环也会在循环正常完成时向 迭代器 发送这个信号。对于generator来说,这实质上是一个无实际意义的操作,因为generator的 迭代器 要首先完成,for..of
循环才能完成。然而,自定义的 迭代器 可能会希望从for..of
循环的消费者那里得到另外的信号。
虽然一个for..of
循环将会自动发送这种信号,你可能会希望手动发送信号给一个 迭代器;你可以通过调用return(..)
来这么做。
如果你在generator内部指定一个try..finally
从句,它将总是被执行,即便是generator从外部被完成。这在你需要进行资源清理时很有用(数据库连接等):
function *something() {
try {
var nextVal;
while (true) {
if (nextVal === undefined) {
nextVal = 1;
}
else {
nextVal = (3 * nextVal) + 6;
}
yield nextVal;
}
}
// 清理用的从句
finally {
console.log( "cleaning up!" );
}
}
前面那个在for..of
中带有break
的例子将会触发finally
从句。但是你可以用return(..)
从外部来手动终结generator的 迭代器 实例:
var it = something();
for (var v of it) {
console.log( v );
// 不要让循环永无休止!
if (v > 500) {
console.log(
// 使generator得迭代器完成
it.return( "Hello World" ).value
);
// 这里不需要`break`
}
}
// 1 9 33 105 321 969
// cleaning up!
// Hello World
当我们调用it.return(..)
时,它会立即终结generator,从而运行finally
从句。而且,它会将返回的value
设置为你传入return(..)
的任何东西,这就是Hellow World
如何立即返回来的。我们现在也不必再包含一个break
,因为generator的 迭代器 会被设置为done:true
,所以for..of
循环会在下一次迭代时终结。
generator的命名大部分源自于这种 消费生产的值 的用法。但要重申的是,这只是generator的用法之一,而且坦白的说,在这本书的背景下这甚至不是我们主要关注的。
但是现在我们更加全面地了解它们的机制是如何工作的,我们接下来可以将注意力转向generator如何实施于异步并发。