JavaScript 生成器的简介

web前端开发

共 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;}
上面代码中 bar 函数很直观地在 b++ 与 a * 2 之间运行,现在假设一下,bar 现在不存在于 foo 函数里,那么怎么才能使单线程的 js 中断 foo 函数去执行bar ,然后再回来继续执行自己没有执行的代码呢。

二、yield 暂停

对于上面的问题,只要我们能够通知 foo 在某个地方暂停下来就可以实现这样的需求。
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 = 40val2 = it2.next(val1 * 5).value; // z = 3, 2中x = 200, val2 = x * z = 600console.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);
// 执行foos1();s1();s1();
// 执行bars2();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; // 1getSomething.next().value; // 9getSomething.next().value; // 33getSomething.next().value; // 105

其中有些令人疑惑的代码大概就是[Symbol.iterable]: function() {return this} 这一行了

这一行的作用是将getSomething 的值也构建成为一个 iterable,现在它既是 iterable 也是迭代器。

for (let v of getSomething) {  console.log(v);}/* 1933105321 */

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 或者未捕获异常引起,会向生成器的迭代器发送一个信号使其终止。


学习更多技能

请点击下方公众号

浏览 29
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报