理解 Generator

这是我参与更文挑战的第16天,活动详情查看:更文挑战


我们都知道,JavaScript 中普通函数只返回一次,或者不返回。

当我们有需要多次返回的需求时,一种特殊的函数 —— Generator 就应运而生了,它可以通过表达式 yield 来多次返回我们想要的值。再配合迭代器,就可以让我们轻松地创建数据流。

下面让我们来认识 Generator 函数。

Generator 函数

为了和普通函数加以区分,Generator 函数规定了一种特殊语法,就是使用 function* ,像这样:

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}
复制代码

Generator 函数的表现也和普通函数不同,它被调用的时候不会直接运行里面的代码,而是先返回一个特殊的 Generator 对象,再用这个对象来执行函数。

像这样:

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}

// "generator function" creates "generator object"
let generator = generateSequence();
alert(generator); // [object Generator]
复制代码

到这里,函数里面的代码还没有被执行。

image.png

想要执行它,就需要调用 Generator 的核心方法 —— next(),它会执行到最近一个 yield <value> 表达式(省略 value的话会返回 undefined)。然后函数就会暂停执行并返回 yield 后面的值。

next()方法返回一个对象,包含以下两个属性:

  • value: yield出来的值
  • done: 函数全部执行完返回true,否则返回false

例如,我们创建一个 generator然后获取它第一个yield的值:

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}

let generator = generateSequence();

let one = generator.next();

alert(JSON.stringify(one)); // {value: 1, done: false}
复制代码

现在函数只执行到了第二行,我们只拿到了第一个返回值。

image.png

我们再次调用 generator.next(), 函数会继续执行并返回下一个 yield:

let two = generator.next();

alert(JSON.stringify(two)); // {value: 2, done: false}
复制代码

image.png

第三次调用,函数将会执行到 return结束语句:

let three = generator.next();

alert(JSON.stringify(three)); // {value: 3, done: true}
复制代码

image.png

现在 Generator 执行完,我们可以看到最终结果返回了 done: truevalue: 3

这时再继续调用将不会有任何动作,会始终返回 {done: true}

问题: function* f(…) 还是 function *f(…) ?
两种都是正确的。
不过更推荐第一种语法,* 表示这是一个 generator 函数,描述的是类型而不是名字,所以和function放在一起更合适。

Generator 是可迭代的

通过 next()方法也许你已经发现了,Generator 是可以迭代的。

我们可以用 for..of 来遍历它:

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}

let generator = generateSequence();

for(let value of generator) {
  alert(value); // 1, then 2
}
复制代码

看起来是不是比调用 .next().value优雅一点?

但是注意,这里只输出了12就结束了,没有输出3

因为 done: true 的时候, for…of 循环就忽略了最后的返回值。所以如果希望用for...of来遍历所有的值,就都要用 yield来返回:

function* generateSequence() {
  yield 1;
  yield 2;
  yield 3;
}

let generator = generateSequence();

for(let value of generator) {
  alert(value); // 1, then 2, then 3
}
复制代码

既然 generator是可以迭代的,我们就可以使用相关特性了,例如扩展运算符 ...:

function* generateSequence() {
  yield 1;
  yield 2;
  yield 3;
}

let sequence = [0, ...generateSequence()];

alert(sequence); // 0, 1, 2, 3
复制代码

这里,...generateSequence() 的作用是 Generator函数迭代的值依次放入数组。

在迭代器中使用Generator

在之前介绍的 迭代器 一节中,我们创建了一个范围迭代对象,返回 form…to

我们复习一下这段代码:

let range = {
  from: 1,
  to: 5,

	// for...of 会在一开始调用这个方法
  [Symbol.iterator]() {
	  // 使用扩展运算符(...)返回一个可迭代的对象:
	  // for...of 循环只使用这个可迭代对象并通过next方法获取值
    return {
      current: this.from,
      last: this.to,

      // for..of 每次循环都会调用next()
      next() {
        // 返回对象 {done:.., value :...}
        if (this.current <= this.last) {
          return { done: false, value: this.current++ };
        } else {
          return { done: true };
        }
      }
    };
  }
};

// 迭代器超出范围后将会返回从 range.from 到 range.to 之间的值
alert([...range]); // 1,2,3,4,5
复制代码

我们也可以用 Generator 函数来定义 Symbol.iterator 方法,下面是一个相同范围但是更简洁的例子:

let range = {
	from: 1,
	to: 5,

	*[Symbol.iterator]() { // 简写版的 [Symbol.iterator]: function*()
		for(let value = this.from; value <= this.to; value++) {
			yield value;
		}
	}
};

alert( [...range] ); // 1,2,3,4,5
复制代码

它的作用原理是 range[Symbol.iterator]() 现在返回了一个 Generator,Generator 方法正好满足了 for…of 迭代时需要的:

  • .next() 方法
  • 返回这种形式的结果: {value: …, done: true/false}

当然这不是一个巧合,Generator 在设计加入 JavaScript 时就考虑到了迭代器的思想,这才让我们实现起来如此容易。

Generator 在保持原有的 range 功能的同时,比原有的代码显得简洁很多。

Generator 组合

Generator 组合是它的一个特殊功能,它允许我们显式地在 Generator 中「嵌入」Generator。

例如,我们有一个生成序列数字的函数:

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) yield i;
}
复制代码

我们希望可以复用它来生成更多复杂序列:

  • 首先,生成数字 0..9(字符编码 48..57
  • 然后,生成大写字母 A..Z(字符编码65..90
  • 最后,生成小写字母 a..z(字符编码 97..122

我们可以用这些字符来创建一个密码(也可以使用特殊字符),我们先生成出来。

在普通函数中,如果要结合其他函数的结果,我们可以调用外部函数,存储结果,最后加入到返回值中。

使用 Generator 的话,我们可以利用它的特殊语法 yield* 来「嵌入」(混合)其他Generator 函数。

一个组合的 Generator:

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) yield i;
}

function* generatePasswordCodes() {

  // 0..9
  yield* generateSequence(48, 57);

  // A..Z
  yield* generateSequence(65, 90);

  // a..z
  yield* generateSequence(97, 122);

}

let str = '';

for(let code of generatePasswordCodes()) {
  str += String.fromCharCode(code);
}

alert(str); // 0..9A..Za..z
复制代码

yield* 指令会委托其他 Generator 函数来执行, yield* gen 会遍历 Generator 函数 gen,然后显式地依次输出它的 yield 值。看起来像是外层的 Generator 函数直接输出了内部的 yield 一样。

把遍历的过程加进来,相当于下面这样:

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) yield i;
}

function* generateAlphaNum() {

  // yield* generateSequence(48, 57);
  for (let i = 48; i <= 57; i++) yield i;

  // yield* generateSequence(65, 90);
  for (let i = 65; i <= 90; i++) yield i;

  // yield* generateSequence(97, 122);
  for (let i = 97; i <= 122; i++) yield i;

}

let str = '';

for(let code of generateAlphaNum()) {
  str += String.fromCharCode(code);
}

alert(str); // 0..9A..Za..z
复制代码

Generator 组合天然就适合把 Generator 流引入到其他 Generator 中,不需要额外使用内存去储存中间结果。

“yield”是双向的

到目前为止,Generator 和可迭代对象很相似,只是用了特殊的语法。事实上他们要更加强大和灵活。

这是因为 yield 是双向的: 不仅向外部返回结果,还可以在内部传递参数。

为了验证,我们传递一个参数来调用 generator.next(arg),这个参数会变成 yield的结果。

function* gen() {
  // 传出一个问题,等待回答
  let result = yield "2 + 2 = ?"; // (*)

  alert(result);
}

let generator = gen();

let question = generator.next().value; // <-- yield returns the value

generator.next(4); // --> pass the result into the generator
复制代码

image.png

  1. 第一次调用generator.next()不需要传参数(传了也会被忽略)。它会开始执行并返回第一个 yield “2+2=?”。这时函数暂停执行,停在 (*) 这行。
  2. 然后,yield 返回的值赋值给了 question变量。
  3. generator.next(4),Generator 恢复执行,4作为结果被引入到了 let result = 4

请注意,外部的代码并不需要立即调用 next(4),隔段时间也没关系,Generator 会等着。

例如:

// resume the generator after some time
setTimeout(() => generator.next(4), 1000);
复制代码

我们可以发现,和普通函数不一样的是,Generator 可以通过 next/yield 来传递参数,从而交换结果。

用另一个例子看得更清楚:

function* gen() {
  let ask1 = yield "2 + 2 = ?";

  alert(ask1); // 4

  let ask2 = yield "3 * 3 = ?"

  alert(ask2); // 9
}

let generator = gen();

alert( generator.next().value ); // "2 + 2 = ?"

alert( generator.next(4).value ); // "3 * 3 = ?"

alert( generator.next(9).done ); // true
复制代码

执行过程:

image.png

  1. 第一个 .next()开始执行,到第一个 yield
  2. 外层代码返回结果
  3. 第二个 next(4)4作为第一个 yield的结果传进去
  4. 到第二个 yield, 正常执行 Generator 的调用
  5. 第三个 next(9)9 作为第二个 yield的结果传进去,继续执行到结束,所以 done: true

这有点像一个乒乓球游戏,每一个 next(value) (除了第一个)都传递了一个值给 Generator, 然后变成了当前 yield 的结果,再又开始下一个 yield

generator.throw

上面的例子可以发现,外层代码可以把值传到 Generator 中作为 yield 的结果。

但是仍然可能会报错,这很正常,错误也是一种结果。

如果传一个错误给 yield, 我们可以调用 generator.throw(err),在执行到 yield这行时就会抛出错误err

例如,这里yield "2 + 2 = ?" 导致了错误。

function* gen() {
  try {
    let result = yield "2 + 2 = ?"; // (1)

    alert("The execution does not reach here, because the exception is thrown above");
  } catch(e) {
    alert(e); // shows the error
  }
}

let generator = gen();

let question = generator.next().value;

generator.throw(new Error("The answer is not found in my database")); // (2)
复制代码

(2) 这一行传入的错误,导致了 (1) 这一行执行异常。这里我们使用了 try...catch来捕获了错误。

如果我们不捕获它,那么就会像普通异常一样,在调用的地方被抛出。

当前调用 generator.throw的是 (2)这行,所以我们可以在这里捕获到它,就相当于:

function* generate() {
  let result = yield2 + 2 = ?”; // Error in this line
}

let generator = generate();

let question = generator.next().value;

try {
  generator.throw(new Error(“The answer is not found in my database”));
} catch(e) {
  alert(e); // shows the error
}
复制代码

如果这里没有捕获到错误,那么一般会在更外层的调用代码中被抛出,如果始终没被捕获,那么就会终止脚本。

总结

  • Generator 是由Generator 函数 function* f(…) {…} 创建的
  • 在 Generator 中存在 yield 运算符
  • 外层代码可以通过 next/yield和 Generator 内部交互结果

在现代 JavaScript 中,Generator 很少被应用。但是它通过函数调用来交换数据的方式很特别,在创建可迭代对象方面也很方便。

下一章我们将学习异步 Generator,在 for await … of中读取异步产生的数据流。

在网页程序中,我们经常使用数据流,所以也是非常有必要学习的。

任务练习

伪随机 Generator

我们常常会碰到一些生成随机数据的需求,比如测试一个随机值是否能输出正常结果。

在JavaScript 中,我们可以使用 Math.random() 来实现随机,但如果用于测试中,我们希望出现异常的数据是可以复现的,所以有时候需要生成一些伪随机数据。

「伪随机」的意思是,如果开始值(种子)不变,那么后面的随机序列也不变。这个种子也叫「随机种子」,它传入第一个数据,然后根据公式生成下一个。整个过程是可以复现的,只需要传入相同的种子即可。

例如我们想下面的公式来生成一组比较分散的值:

next = previous * 16807 % 2147483647
复制代码

假设我们用1来作为种子,将生成以下值:

  1. 16807
  2. 282475249
  3. 1622650073
  4. …等等…

现在给你一个任务,创建一个 Generator 函数 pseudoRandom(seed),获取 seed 值然后用公式生成结果。

用例如下:

let generator = pseudoRandom(1);

alert(generator.next().value); // 16807
alert(generator.next().value); // 282475249
alert(generator.next().value); // 1622650073
复制代码

答案:

第一种方法:

function* pseudoRandom(seed) {
  let value = seed;

  while(true) {
    value = value * 16807 % 2147483647
    yield value;
  }

};

let generator = pseudoRandom(1);

alert(generator.next().value); // 16807
alert(generator.next().value); // 282475249
alert(generator.next().value); // 1622650073
复制代码

第二种方法:使用普通函数来实现,只是不能使用 for..of来迭代,并且无法用到组合 Generator。

function pseudoRandom(seed) {
  let value = seed;

  return function() {
    value = value * 16807 % 2147483647;
    return value;
  }
}

let generator = pseudoRandom(1);

alert(generator()); // 16807
alert(generator()); // 282475249
alert(generator()); // 1622650073
复制代码

参考内容: javascript.info/generators

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享