这是我参与更文挑战的第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]
到这里,函数里面的代码还没有被执行。
想要执行它,就需要调用 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}
现在函数只执行到了第二行,我们只拿到了第一个返回值。
我们再次调用 generator.next()
, 函数会继续执行并返回下一个 yield
:
let two = generator.next();
alert(JSON.stringify(two)); // {value: 2, done: false}
第三次调用,函数将会执行到 return
结束语句:
let three = generator.next();
alert(JSON.stringify(three)); // {value: 3, done: true}
现在 Generator 执行完,我们可以看到最终结果返回了 done: true
和 value: 3
。
这时再继续调用将不会有任何动作,会始终返回 {done: true}
。
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
优雅一点?
但是注意,这里只输出了1
和2
就结束了,没有输出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
- 第一次调用
generator.next()
不需要传参数(传了也会被忽略)。它会开始执行并返回第一个yield “2+2=?”
。这时函数暂停执行,停在(*)
这行。 - 然后,
yield
返回的值赋值给了question
变量。 - 到
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
执行过程:
- 第一个
.next()
开始执行,到第一个 yield - 外层代码返回结果
- 第二个
next(4)
把4
作为第一个yield
的结果传进去 - 到第二个
yield
, 正常执行 Generator 的调用 - 第三个
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 = yield “2 + 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
来作为种子,将生成以下值:
16807
282475249
1622650073
- …等等…
现在给你一个任务,创建一个 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
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!