打破完整运行
在第1 章中,我们解释了JavaScript 开发者在代码中几乎普遍依赖的一个假定:一个函数一旦开始执行,就会运行到结束,期间不会有其他代码能够打断它并插入其间。
可能看起来似乎有点奇怪,不过ES6 引入了一个新的函数类型,它并不符合这种运行到结束的特性。这类新的函数被称为生成器。
var x = 1;
function foo() {
x++;
bar(); // <-- 这一行是什么作用?
console.log("x:", x);
}
function bar() {
x++;
}
foo(); // x: 3
在这个例子中,我们确信bar() 会在x++ 和console.log(x) 之间运行。但是,如果bar()并不在那里会怎样呢?显然结果就会是2,而不是3。
现在动脑筋想一下。如果bar() 并不在那儿,但出于某种原因它仍然可以在x++ 和console.log(x) 语句之间运行,这又会怎样呢?这如何才会成为可能呢?
如果是在抢占式多线程语言中,从本质上说,这是可能发生的,bar() 可以在两个语句之间打断并运行。但JavaScript 并不是抢占式的,(目前)也不是多线程的。然而,如果foo() 自身可以通过某种形式在代码的这个位置指示暂停的话,那就仍然可以以一种合作式的方式实现这样的中断(并发)。
下面是实现这样的合作式并发的ES6 代码:
var x = 1;
function* foo() {
x++;
yield; // 暂停!
console.log("x:", x);
}
function bar() {
x++;
}
很可能你看到的其他多数JavaScript 文档和代码中的生成器声明格式都是function* foo() { .. },而不是我这里使用的function foo() { .. }:唯一区别是 位置的风格不同。这两种形式在功能和语法上都是等同的,还有一种是function*foo(){ .. }(没有空格)也一样。两种风格,各有优缺,但总体上我比较喜欢function foo.. 的形式,因为这样在使用foo()来引用生成器的时候就会比较一致。如果只用foo() 的形式,你就不会清楚知道我指的是生成器还是常规函数。这完全是一个风格偏好问题。
现在,我们要如何运行前面的代码片段,使得bar() 在*foo() 内部的yield 处执行呢?
// 构造一个迭代器it来控制这个生成器
var it = foo();
// 这里启动foo()!
it.next();
x; // 2
bar();
x; // 3
it.next(); // x: 3
(1) it = foo() 运算并没有执行生成器foo(),而只是构造了一个迭代器(iterator),这个
迭代器会控制它的执行。后面会介绍迭代器。
(2) 第一个it.next() 启动了生成器foo(),并运行了foo() 第一行的x++。
(3) foo() 在yield 语句处暂停,在这一点上第一个it.next() 调用结束。此时foo() 仍
在运行并且是活跃的,但处于暂停状态。
(4) 我们查看x 的值,此时为2。
(5) 我们调用bar(),它通过x++ 再次递增x。
(6) 我们再次查看x 的值,此时为3。
(7) 最后的it.next() 调用从暂停处恢复了生成器foo() 的执行,并运行console.log(..)
语句,这条语句使用当前x 的值3。
因此,生成器就是一类特殊的函数,可以一次或多次启动和停止,并不一定非得要完成。尽管现在还不是特别清楚它的强大之处,但随着对本章后续内容的深入学习,我们会看到它将成为用于构建以生成器作为异步流程控制的代码模式的基础构件之一。
迭代消息传递
function* foo(x) {
var y = x * (yield);
return y;
}
var it = foo(6);
// 启动foo(..)
it.next();
var res = it.next(7);
res.value; // 42
首先,传入6 作为参数x。然后调用it.next(),这会启动foo(..)。在foo(..) 内部,开始执行语句var y = x ..,但随后就遇到了一个yield 表达式。它就会在这一点上暂停*foo(..)(在赋值语句中间!),并在本质上要求调用代码为yield表达式提供一个结果值。接下来,调用it.next( 7 ),这一句把值7 传回作为被暂停的yield 表达式的结果。
两个问题的故事
消息是双向传递的——yield.. 作为一个表达式可以发出消息响应next(..) 调用,next(..) 也可以向暂停的yield 表达式发送值。
function* foo(x) {
var y = x * (yield "Hello"); // <-- yield一个值!
return y;
}
var it = foo(6);
var res = it.next(); // 第一个next(),并不传入任何东西
res.value; // "Hello"
res = it.next(7); // 向等待的yield传入7
res.value; // 42
yield .. 和next(..) 这一对组合起来,在生成器的执行过程中构成了一个双向消息传递系统。
我们并没有向第一个next() 调用发送值,这是有意为之。只有暂停的yield才能接受这样一个通过next(..) 传递的值,而在生成器的起始处我们调用第一个next() 时,还没有暂停的yield 来接受这样一个值。规范和所有兼容浏览器都会默默丢弃传递给第一个next() 的任何东西。传值过去仍然不是一个好思路,因为你创建了沉默的无效代码,这会让人迷惑。因此,启动生成器时一定要用不带参数的next()。
第一个next() 调用(没有参数的)基本上就是在提出一个问题:“生成器*foo(..) 要给我的下一个值是什么”。谁来回答这个问题呢?第一个yield "hello" 表达式。
但是,稍等!与yield 语句的数量相比,还是多出了一个额外的next()。所以,最后一个it.next(7) 调用再次提出了这样的问题:生成器将要产生的下一个值是什么。但是,再没有yield 语句来回答这个问题了,是不是?那么谁来回答呢?
return 语句回答这个问题!
如果你的生成器中没有return 的话——在生成器中和在普通函数中一样,return 当然不是必需的——总有一个假定的/ 隐式的return;(也就是 return undefined;),它会在默认情况下回答最后的it.next(7) 调用提出的问题。
多个迭代器
从语法使用的方面来看,通过一个迭代器控制生成器的时候,似乎是在控制声明的生成器函数本身。但有一个细微之处很容易忽略:每次构建一个迭代器,实际上就隐式构建了生成器的一个实例,通过这个迭代器来控制的是这个生成器实例。
同一个生成器的多个实例可以同时运行,它们甚至可以彼此交互:
function* foo() {
var x = yield 2;
z++;
var y = yield(x * z);
console.log(x, y, z);
}
var z = 1;
var it1 = foo();
var it2 = foo();
var val1 = it1.next().value; // 2 <-- yield 2
var val2 = it2.next().value; // 2 <-- yield 2
val1 = it1.next(val2 * 10).value; // 40 <-- x:20, z:2
val2 = it2.next(val1 * 5).value; // 600 <-- x:200, z:3
it1.next(val2 / 2); // y:300
// 20 300 3
it2.next(val1 / 4); // y:10
// 200 10 3
同一个生成器的多个实例并发运行的最常用处并不是这样的交互,而是生成器在没有输入的情况下,可能从某个独立连接的资源产生自己的值。下一节中我们会详细介绍值产生。
生成器产生值
生产者与迭代器
可以为我们的数字序列生成器实现标准的迭代器接口:
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
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 之后自动停止。这对于在一组数据上循环很方便。
iterable
前面例子中的something 对象叫作迭代器,因为它的接口中有一个next() 方法。而与其紧密相关的一个术语是iterable(可迭代),即指一个包含可以在其值上迭代的迭代器的对象。
从ES6 开始,从一个iterable 中提取迭代器的方法是:iterable 必须支持一个函数,其名称是专门的ES6 符号值Symbol.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
..
生成器迭代器
可以把生成器看作一个值的生产者,我们通过迭代器接口的next() 调用一次提取出一个值。
所以,严格说来,生成器本身并不是iterable,尽管非常类似——当你执行一个生成器,就得到了一个迭代器。
停止生成器
在前面的例子中,看起来似乎*something() 生成器的迭代器实例在循环中的break 调用之后就永远留在了挂起状态。
其实有一个隐藏的特性会帮助你管理此事。for..of 循环的“异常结束”(也就是“提前终止”),通常由break、return 或者未捕获异常引起,会向生成器的迭代器发送一个信号使其终止。