JavaScript 生成器的简介
共 6607字,需浏览 14分钟
·
2021-11-19 20:54
来源 | https://mp.weixin.qq.com/s/IdvvFfvgrwAt7DTJKnxGYA
一、单线程的 js
var a = 3;
var b = 2;
function foo() {
a = a * 2;
bar();
b++;
console.log(a, b); // 7, 5
}
function bar() {
a++;
b = b * 2;
}
二、yield 暂停
var a = 3;
var b = 2;
function* foo() {
// 声明生成器
a = a * 2;
yield;
b++;
console.log(a, b);
}
function bar() {
a++;
b = b * 2;
}
有些地方使用的是function *foo(){...} 的形式
那么现在要如何运行代码才能达到想要的效果呢
/* 构造一个迭代器it来控制生成器foo,此时foo完全没有被执行 */
var it = foo();
/* foo开始执行,一直到遇到yield才停止,此时a = 6, b = 2 */
it.next();
console.log(a, b); // 6, 2
/* foo让出线程,执行bar,此时a = 7, b = 4 */
bar();
console.log(a, b); // 7, 4
/* foo继续执行,此时a = 7, b = 5 */
it.next(); // 7, 5
生成器是一类特殊的函数,可以一次或多次启动与停止,而且不一定非要完成
三、输入与输出
生成器特殊归特殊,它始终都是一个函数,依然具有一些函数的基本特性,例如接受参数(输入),返回值(输出)
function* foo(x, y) {
return x * y;
}
var it = foo(6, 7);
var res = it.next();
console.log(res);
// {value: 42, done: true}
next 的调用结果是一个对象,拥有一个 value 属性,持有从 foo 返回的值,也就是说,yield 会导致生成器在执行过程中发送出一个值,有点类似上面的return
3.1 迭代消息传递
除了能接收参数并提供返回值,生成器还提供了更强大的内建消息输入输出能力,通过 yield 和 next 实现
function* foo(x) {
var y = x * (yield);
return y;
}
var it = foo(6);
it.next();
var res = it.next(7);
console.log(res);
// { value: 42, done: true }
首先 6 作为参数 x,调用it.next() ,启动foo
在foo 内部,执行语句var y = x... 时遇到yield 表达式,在此处(赋值语句中间)暂停foo ,并要求调用代码为yield 的表达式提供一个结果值,接下来调用it.next(7) ,这一句把 7 传回作为被暂停的yield 表达式的结果
3.2 从两个视角看next 与yield
一般来讲,next 的数量要比yield 的数量多一个,这在理解代码时会给人一种不协调不匹配的感觉
只考虑生成器代码
var y = x * yield;
return y;
第一个yield 提出一个问题:我这里应该插入什么值?
谁来回答这个问题呢,第一个next 已经运行使得生成器启动并运行到此处,它显然无法回答这个问题,因此必须由第二个next 调用回答第一个yield 提出的问题
这就是不匹配——第二个next对第一个yield?
接下来转换一下视角,从迭代器角度看问题
在此之前需要再次解释以下yield的消息传递,它是双向的,前面我们只提了next向暂停的yield发送值,下面看一下yield发出消息供next使用。
function* foo(x) {
var y = x * (yield "hello world");
return y;
}
var it = foo(6);
/* 第一个next不传入任何东西,实际上就算传了也会被浏览器悄咪咪丢掉 */
var res = it.next();
console.log(res); // { value: 'hello world', done: false }
/* 给等待的yield传一个7 */
res = it.next(7);
console.log(res); // { value: 42, done: true }
第一个next 调用提出一个问题:foo要给我的下一个值是什么?
谁来回答呢?第一个yield "hello world" 表达式
这里没有不匹配的问题
但是,对于最后一个next ,也就是it.next(7) 提出的foo要给我的下一个值是什么的问题,没有yield 来回答它了,那么由谁来回答呢
答案是return 语句
如果生成器中没有return,则会有隐式的return undefined 来回答
四、多个迭代器
从语法使用的方面来看,通过一个迭代器控制生成器的时候,似乎是在控制声明的生成器函数本身,但有一个细节需要注意,每次构建一个迭代器,实际上就隐式构建了生成器的一个实例,通过这个迭代器来控制其对应的生成器实例
同一个生成器的多个实例可以同时运行,甚至彼此交互。
function* foo(i) {
var x = yield 2;
z++;
var y = yield x * z;
console.log({ i, x, y, z });
}
var z = 1;
var it1 = foo(1);
var it2 = foo(2);
var val1 = it1.next().value;
var val2 = it2.next().value;
console.log({ val1, val2 }); // { val1: 2, val2: 2 }
val1 = it1.next(val2 * 10).value; // z = 2, 1中x = 20, val1 = x * z = 40
val2 = it2.next(val1 * 5).value; // z = 3, 2中x = 200, val2 = x * z = 600
console.log({ val1, val2 }); // { val1: 40, val2: 600 }
it1.next(val2 / 2); // { i: 1, x: 20, y: 300, z: 3 }
it2.next(val1 / 4); // { i: 2, x: 200, y: 10, z: 3 }
再来看一个例子。
var a = 1;
var b = 2;
function foo() {
a++;
b = b * a;
a = b + 3;
}
function bar() {
b--;
a = 8 + b;
b = a * 2;
}
上面是一串普通的 js 代码,对于普通的 JS 函数,显然要么先执行 foo,要么先执行 bar,二者不能交替执行
但是,使用生成器的话,交替执行显然是可能的
var a = 1;
var b = 2;
function* foo() {
a++;
yield;
b = b * a;
a = (yield b) + 3;
}
function* bar() {
b--;
yield;
a = (yield 8) + b;
b = a * (yield 2);
}
根据每一步调用的相对顺序的不同,上面的程序能产生多种不同的结果
首先构建一个辅助函数来控制迭代器
function step(gen) {
// 初始化一个生成器来创建迭代器it
var it = gen();
var last;
// 返回一个函数,每次被调用都会将迭代器向前迭代一步,前面yield发出的值会在下一步发送回去
return function () {
// 不管yield出来的是什么下一次都把它原原本本的传回去
last = it.next(last).value;
};
}
下面我们先从最基本的情况开始,让 foo 在 bar 之前执行结束。
var s1 = step(foo);
var s2 = step(bar);
// 执行foo
s1();
s1();
s1();
// 执行bar
s2();
s2();
s2();
s2();
console.log(a, b); // 11, 22
然后我们使二者交替进行
var s1 = step(foo);
var s2 = step(bar);
s2();
// b--, b = 1, 遇到yield,暂停!last接受第一个yield发出的值undefined
s2();
// 第一个yield返回值为undefined(无影响),执行a = ...遇到yield,暂停!last 接受第二个yield发出的值8
s1();
/* a++, a = 2, 遇到yield,暂停!last接受第一个yield发出的值undefined */
s2();
// 第二个yield返回值为8,a = 8 + b = 8 + 1 = 9, 执行b = 9 * ...遇到yield,暂停!last 接受第三个yield发出的值2
s1();
/* 第一个yield返回值为undefined(无影响),b = b * a = 1 * 9 = 9, 执行a = ..., 遇到yield,暂停!last接受第二个yield发出的值b,也就是9 */
s1();
/* 第二个yield返回值为9,a = 9 + 3 = 12,s1执行完毕,foo结束 */
s2();
// 第三个yield返回值为2,b = 9 * 2 = 18,s2执行完毕,bar结束
console.log(a, b); // 12, 18
五、迭代器与生成器
前面说了那么多“迭代器”与“生成器”,现在具体来谈一谈这二者是什么东东
5.1 迭代器
5.1.1 iterable
在 JavaScript 中,迭代器是一个对象,它定义一个序列,并在终止时可能返回一个返回值。更具体地说,迭代器是通过使用 next() 方法实现 Iterator protocol 的任何一个对象,该方法返回具有两个属性的对象:value,这是序列中的 next值;和 done,如果已经迭代到序列中的最后一个值,则它为 true 。如果 value 和 done 一起存在,则它是迭代器的返回值。
从 ES6 开始,从一个 iterable 中提取迭代器的方法是:iterable 必须支持以后函数,其名称是Symbol.iterable ,调用这个函数时,它会返回一个迭代器,通常每次调用会返回一个全新的迭代器
后面我们根据例子具体分析
5.1.2 生产者与迭代器
假定现在你要产生一系列值,每一个值都与前面一个有特定的关系,要实现这一点,需要一个有状态的生产者能记住其生成的最后一个值
先用闭包整一个
var getSomething = (function () {
var nextVal;
return function () {
if (nextVal === undefined) nextVal = 1;
else nextVal = nextVal * 3 + 6;
console.log(nextVal);
return nextVal;
};
})();
getSomething();
getSomething();
getSomething();
getSomething();
再试着对它修改,将它改为一个标准的迭代器接口。
var getSomething = (function () {
var nextVal;
return {
[Symbol.iterator]: function () {
return this;
},
next: function () {
if (nextVal === undefined) nextVal = 1;
else nextVal = nextVal * 3 + 6;
return { done: nextVal > 500, value: nextVal };
},
};
})();
getSomething.next().value; // 1
getSomething.next().value; // 9
getSomething.next().value; // 33
getSomething.next().value; // 105
其中有些令人疑惑的代码大概就是[Symbol.iterable]: function() {return this} 这一行了
这一行的作用是将getSomething 的值也构建成为一个 iterable,现在它既是 iterable 也是迭代器。
for (let v of getSomething) {
console.log(v);
}
/*
1
9
33
105
321
*/
5.2 生成器
可以把生成器看作一个值的生产者,我们通过迭代器接口的next 调用一次提取出一个值,所以严格来讲,生成器本身并不是 iterrable,尽管执行一个生成器就得到了一个迭代器
如果使用生成器来实现前面的getSometing :
function* getSomething() {
var nextVal;
while (true) {
if (nextVal === undefined) nextVal = 1;
else nextVal = nextVal * 3 + 6;
yield nextVal;
}
}
生成器会在每个yield 处暂停,函数getSomething 的作用域会被保持,也就意味着不需要闭包在调用之间保持变量状态
上面代码也同样可以使用for...of 循环
// 注意这里是getSomething(),得到了它的迭代器来进行循环的
for (var v of getSomething()) {
console.log(v);
// 不要死循环!
if (v > 500) {
break;
}
}
但是上面的代码看起来getSomething 在break执行之后被永远挂起了。
不过并非如此,for...of 循环的“异常结束”,通常由 break,return 或者未捕获异常引起,会向生成器的迭代器发送一个信号使其终止。
学习更多技能
请点击下方公众号