java双行道,程序员元宵加班搞代码:生成器

程序员元宵加班搞代码:生成器

常规函数只返回一个值(或不返回任何值)。

生成器可以按需一个接一个地返回(“yield”)多个值。它们与可迭代对象一起工作得很好,允许轻松地创建数据流。

生成器函数

为了创建一个生成器,我们需要一个特殊的语法结构:function*,也就是所谓的“生成器函数”。

它是这样的:

function* generateSequence(){

yield 1;

yield 2;

return 3;

}

生成器函数的行为与常规函数不同。当这样的函数被调用时,它不会运行它的代码。相反,它返回一个特殊的对象,称为“生成器对象”,来管理执行。

来看看吧:

function* generateSequence(){

yield 1;

yield 2;

return 3;

}

// "generator function" creates "generator object"

let generator = generateSequence();

alert(generator); // [object Generator]

函数代码还没有开始执行:

5633f0d51df0b696196c2a697209c0e0.png

生成器的主要方法是next()。当被调用时,它会一直执行直到最近的yield 语句(value可以省略,那么它是未定义的)。然后函数暂停执行,生成的值返回到外部代码。

next()的结果总是一个具有两个属性的对象:

value:生成的值。

done:如果函数代码已经完成,则为

true,否则为

false。

例如,这里我们创建了生成器,并获得它的第一个生成值:

function* generateSequence(){

yield 1;

yield 2;

return 3;

}

let generator = generateSequence();

let one = generator.next();

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

到目前为止,我们只得到第一个值,函数执行在第二行:

fd870d319702990bbaf0f9cfc28e9f54.png

让我们再次调用generator.next()``。它继续执行代码并返回下一个yield`:

let two = generator.next();

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

并且,如果我们第三次调用它,执行将到达结束函数的return语句:

eac6ef53f31f66f41dfbdae0843bcb63.png

现在生成器完成了。我们应该看到done:true和process value:3作为最终结果。

对generator.next()的新调用不再有意义。如果我们执行它们,它们将返回相同的对象:{done: true}。

function* f(…) or function *f(…)?

两种语法都是正确的。

但通常首选第一种语法,因为星号*表示它是一个生成器函数,它描述的是类型,而不是名称,所以它应该坚持使用function关键字。

生成器是可迭代的

在查看next()方法时,您可能已经猜到生成器是可迭代的。

我们可以使用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!

这是因为对. .执行时忽略最后一个值:true。因此,如果我们想让for..of显示所有的结果。对于,我们必须退让:

function* generateSequence(){

yield 1;

yield 2;

yield 3;

}

let generator = generateSequence();

for(let value of generator) {

alert(value); // 1, then 2, then 3

}

由于生成器是可迭代的,我们可以调用所有相关的功能,例如spread语法…:

function* generateSequence(){

yield 1;

yield 2;

yield 3;

}

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

alert(sequence); // 0, 1, 2, 3

在上面的代码中,…generateSequence()将可迭代生成器对象转换为一组项(请参阅Rest参数和spread语法一章中的spread语法)。

Using generators for iterables

let range = {

from: 1,

to: 5,

// for..of range calls this method once in the very beginning

[Symbol.iterator]() {

// ...it returns the iterator object:

// onward, for..of works only with that object, asking it for next values

return {

current: this.from,

last: this.to,

// next() is called on each iteration by the for..of loop

next() {

// it should return the value as an object {done:.., value :...}

if (this.current <= this.last) {

return { done: false, value: this.current++ };

} else {

return { done: true };

}

}

};

}

};

// iteration over range returns numbers from range.from to range.to

alert([...range]); // 1,2,3,4,5

通过将generator函数作为Symbol.iterator提供,可以使用它进行迭代。

这里是相同的范围,但更紧凑:

let range = {

from: 1,

to: 5,

*[Symbol.iterator]() { // a shorthand for [Symbol.iterator]: function*()

for(let value = this.from; value <= this.to; value++) {

yield value;

}

}

};

alert( [...range] ); // 1,2,3,4,5

这是可行的,因为range[Symbol.iterator]()现在返回一个生成器,而生成器方法正是for..of预计:

.next() 方法

返回

{value: ..., done: true/false}

当然,这不是巧合。在JavaScript语言中添加生成器时考虑到了迭代器,以便轻松实现它们。

带有生成器的变体比range的原始可迭代代码要简洁得多,并且保持了相同的功能。

生成器可以永远生成值

在上面的例子中,我们生成了有限序列,但是我们也可以生成一个永久生成值的生成器。例如,伪随机数的无穷序列。

生成器组合

生成器组合是生成器的一个特殊特性,它允许透明地将生成器相互“嵌入”。

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

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)

我们可以使用这个序列,例如通过选择其中的字符来创建密码(也可以添加语法字符),但是让我们先生成它。

在常规函数中,为了组合来自多个其他函数的结果,我们调用它们,存储结果,然后在最后连接。

对于生成器,有一个特殊的yield*语法来“嵌入”(组合)一个生成器到另一个生成器中。

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*指令将执行委托给另一个生成器。这个术语意味着yield* gen在生成器生成器上迭代,并透明地将它的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

生成器组合是一种将一个生成器流插入另一个生成器流的自然方法。它不使用额外的内存来存储中间结果。

yield 是一条双行道

在此之前,生成器类似于iterable对象,使用特殊的语法来生成值。但事实上,它们更强大、更灵活。

这是因为yield是一条双向通道:它不仅向外部返回结果,还可以在生成器内部传递值。

为此,我们应该调用generator.next(arg),并带一个参数。这个论证就是yield的结果。

让我们来看一个例子:

function* gen(){

// Pass a question to the outer code and wait for an answer

let result = yield "2 + 2 = ?"; // (*)

alert(result);

}

let generator = gen();

let question = generator.next().value; // 

generator.next(4); // --> pass the result into the generator

30e45408fcabab2485e2418ff9733b86.png

第一个调用generator.next()应始终不带实参(如果传入,实参将被忽略)。它开始执行并返回第一个yield的结果“2+2=?”。此时生成器暂停执行,并保持在(*)行上。

然后,如上图所示,yield的结果进入调用代码中的question变量。

在generator.next(4)上,生成器恢复,结果为4:let result = 4。

请注意,外部代码不必立即调用next(4)。这可能需要时间。这不是问题:发电机会等待。

例如:

// resume the generator after some time

setTimeout(() => generator.next(4), 1000);

正如我们所见,与常规函数不同,生成器和调用代码可以通过在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

执行情况:

b2a7d3899619138d0aff5d38d2e6e127.png

第一个.next()开始执行…它到达第一个yield。

结果返回到外部代码。

第二个.next(4)将4作为第一个yield的结果传递回生成器,并继续执行。

它达到了第二个收益,它成为生成器调用的结果。

第三个next(9)将9作为第二个yield的结果传递给生成器,并继续执行到达函数末尾的操作,完成:true。

这就像一个“乒乓球”游戏。每个next(值)(不包括第一个)都向生成器传递一个值,该值成为当前收益率的结果,然后返回下一个收益率的结果。

generator.throw

正如我们在上面的例子中所观察到的,外部代码可以将一个值传递给生成器,作为yield的结果。

但是它也可能在那里引发(抛出)错误。这是很自然的,因为错误是一种结果。

要将错误传递给yield,应该调用generator.throw(err)。在这种情况下,在具有该yield的行中抛出err。

例如,这里“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)

382/5000 在第(2)行抛出到生成器中的错误会导致第(1)行中出现yield异常。在上面的例子中,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

}

如果我们没有在那里捕获到错误,那么它就会像往常一样落到外部调用代码(如果有的话),如果没有捕获到,就会终止脚本。

总结

生成器是由生成器函数function* f(…){…}创建的。

生成器(仅)内部存在yield操作符。

外部代码和生成器可以通过next/yield调用交换结果。

在现代JavaScript中,很少使用生成器。但有时它们也会派上用场,因为函数在执行期间与调用代码交换数据的能力是非常独特的。当然,它们对于制作可迭代对象非常有用。

另外,在下一章中,我们将学习异步生成器,它用于读取异步生成的数据流(例如通过网络进行分页读取)。的循环。

在网络编程中,我们经常使用流数据,所以这是另一个非常重要的用例。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值