Generators
JavaScript Generators (JavaScript生成器)
In this chapter, we are going to observe the JavaScript generators. Generators are functions that you can use for controlling the iterator. In comparison with regular functions that return a single value or nothing, generators return multiple values in a sequence, one after another. They operate great with iterables and allow creating data streams with ease. (在本章中,我们将学习JavaScript生成器。 生成器是可用于控制迭代器的函数。 与返回单个值或不返回任何值的常规函数相比,生成器在一个序列中返回多个值,一个接一个。 它们可以很好地与可迭代对象配合使用,并允许轻松创建数据流。)
Generator Functions
Generator Functions (生成器功能)
For creating a generator, it is necessary to use a particular syntax construct: function*, known as a generator function.
Here is a case in point:
function* generate() {
yield 1;
yield 2;
return 3;
}
Generator functions are different from regular ones. Every time a function like that is called, it doesn’t run its code. Alternatively, it returns a particular object, called “generator object” for managing the execution. (生成器功能与常规功能不同。每次调用这样的函数时,它都不会运行其代码。或者,它返回一个特定的对象,称为“生成器对象” ,用于管理执行。)
For instance:
function* generate() {
yield 1;
yield 2;
return 3;
}
// "generator function" creates "generator object"
let generator = generate();
console.log(generator); // [object Generator]
The primary method of a generator is next(). When you call it, it runs the execution till the nearest yield <value> statement. Afterward, the function execution suspends, and the yielded value on its turn is returned to the outer code.
The next()’ result is always an object with the following two properties:
the value: that is the yielded value;
done: true in case the function code has finished; otherwise, it’s false.
In the example below, the generator is created, getting its first yielded value:
function* generate() {
yield 1;
yield 2;
return 3;
}
let generator = generate();
let oneValue = generator.next();
console.log(JSON.stringify(oneValue)); // {value: 1, done: false}
As of now, the first value is received, and the function execution is demonstrated on the second line. (到目前为止,接收到第一个值,并在第二行演示函数的执行。)
Now, let’s try to call the generator.next() once more. It restores the code execution, returning the next yield, like this:
function* generate() {
yield 1;
yield 2;
return 3;
}
let generator = generate();
let oneValue = generator.next();
console.log(JSON.stringify(oneValue)); // {value: 1, done: false}
let twoValue = generator.next();
console.log(JSON.stringify(twoValue)); // {value: 2, done: false}
In case you call it the third time, the execution will reach the return statement, which ends the function like this:
function* generate() {
yield 1;
yield 2;
return 3;
}
let generator = generate();
let oneValue = generator.next();
console.log(JSON.stringify(oneValue)); // {value: 1, done: false}
let twoValue = generator.next();
console.log(JSON.stringify(twoValue)); // {value: 2, done: false}
let threeValue = generator.next();
console.log(JSON.stringify(threeValue)); // {value: 3, done: true}
Now, the generator is considered done. You can see it from done:true and process value:3 as the culminating result. Further calls to generator.next() will make no sense. If you decide to do them, they will return the same object {done: true}.
Now, let’s review the following syntaxes: function* f(…) and function *f(…). Both of these syntaxes are correct. But, as a rule, the first one is more preferable in the sense the * indicates that it’s a generator function, it outlines the kind, not the name, so it needs to stick with the function keyword.
Generators are Iterable
Generators are Iterable (生成器可迭代)
Looking at the done:true method, you may probably guess that generators are iterable.
It is possible to loop over their values using for..of, like here:
function* generate() {
yield 1;
yield 2;
return 3;
}
let generator = generate();
for (let value of generator) {
console.log(value); // 1, then 2
}
Perhaps, you will agree that it is much more helpful than calling .next().value. (也许,您会同意它比调用.next () .value更有帮助。)
But, please, take into consideration that the example above shows 1, then 2, but it doesn’t show 3. (但是,请注意,上面的示例显示1 ,然后是2 ,但它没有显示3。)
The reason is that for..of iteration ignores the last value when done: true. Hence, if you wish all the results to be demonstrated by for..of, you should return them by yield, like this:
function* generate() {
yield 1;
yield 2;
yield 3;
}
let generator = generate();
for (let value of generator) {
console.log(value); // 1, then 2, then 3
}
As generators are iterable, it is possible to call all related functionality. In other words, the spread syntax …. (由于生成器是可迭代的,因此可以调用所有相关功能。换句话说,传播语法……)
Let’s take a look at an example:
function* generate() {
yield 1;
yield 2;
yield 3;
}
let sequence = [0, ...generate()];
console.log(sequence); // 0, 1, 2, 3
Generator Composition
Generator Composition (发电机组成)
A unique feature of generators that allows embedding generators in each other transparently is called generator composition. (生成器的一个独特功能是允许彼此透明地嵌入生成器,称为生成器组合。)
Let’s consider a function, which generates a sequence of numbers:
function* generate(start, end) {
for (let i = start; i <= end; i++) yield i;
}
There is also the option of reusing it to generate a more complicated order, like here:
First, digits 0..9 (the character codes are 48…57). (-首先,数字0… 9 (字符代码为48… 57 )。)
Followed by uppercase letters A..Z ( the character codes are 65…90). (-后跟大写字母A.. Z (字符代码为65… 90 )。)
Followed by lowercase letters (the character codes 97…122). (-后跟小写字母(字符代码97… 122 )。)
sequence is very convenient for creating passwords by selecting characters from it. (通过从中选择字符来创建密码非常方便。)
Within a regular function, to mix results from multiple other functions, you can call them, store the results, and join at the end. (在常规函数中,要混合来自多个其他函数的结果,可以调用它们,存储结果,并在最后加入。)
Generators have a unique yield* syntax for composing one generator into another. (生成器具有独特的yield *语法,用于将一个生成器组合到另一个生成器中。)
The composed generator will look as follows:
function* generate(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* generateCodes() {
// 0..9
(0 → 9)
yield* generate(48, 57);
// A..Z
(A-Z)
yield* generate(65, 90);
// a..z
(A → Z)
yield* generate(97, 122);
}
let str = '';
for (let code of generateCodes()) {
str += String.fromCharCode(code);
}
console.log(str); // 0..9A..Za..z
So, the execution is delegated to another generator by the yield* directive. (因此,执行通过yield *指令委托给另一个生成器。)
This term signifies that yield* gen iterates over the generator gen and forwards its yields outside transparently. Just as though the outer generator yielded the values. (该术语表示yield * gen在发电机gen上迭代,并将其yield透明地转发到外部。就好像外部生成器产生了值一样。)
So, the result will be as follows:
function* generate(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* generateCodes() {
// 0..9
(0 → 9)
yield* generate(48, 57);
// A..Z
(A-Z)
yield* generate(65, 90);
// a..z
(A → Z)
yield* generate(97, 122);
}
let str = '';
for (let code of generateCodes()) {
str += String.fromCharCode(code);
}
console.log(str); // 0..9A..Za..z
It can be assumed that a generator composition is a natural way of inserting a flow of one generator into another. No supplementary memory is used to store intermediate results. (可以假设发电机组合物是将一个发电机的流插入另一个发电机的自然方式。不使用补充内存来存储中间结果。)
“Yield” is a Two-way Street
“Yield” is a Two-way Street (“Yield”是一条双向街道)
As it was already stated, generators are like iterable objects with a unique syntax for generating values. But, they are even more flexible and powerful. (如前所述,生成器就像具有生成值的独特语法的可迭代对象。但是,它们更加灵活和强大。)
So, yield is considered a two-way street: it can both return the result to the outside and pass the value inside the generator.
To implement that, you need to call the generator.next(arg) using an argument. The argument will become the result of the yield. (要实现此功能,需要使用参数调用generator.next (arg)。参数将成为收益率的结果。)
For instance:
function* gen() {
// Pass a question to external code and wait for an answer
(//将问题传递给外部代码并等待答案)
let result = yield "2 * 2 = ?"; // (*)
console.log(result);
}
let generator = gen();
let question = generator.next().value; // yield returns the value
generator.next(4); // pass the result into the generator
The first generator.next() is always made without using any argument ( if an argument is passed, it is ignored). It begins the execution, returning the result of the first yield yield “22=?”. At this stage, the generator suspends the execution, staying on the line (). Afterward, the result of the yield gets into the question variable inside the calling call. On the generator.next(4), the generator restarts and 4 gets in as aresult, like this: let result = 4
Please, take into consideration that the outer code doesn’t have to call next(4) immediately. It usually takes time. The generator, on its turn, waits. (请注意,外部代码不必立即调用next (4)。这通常需要时间。发电机轮流等待。)
Take a look at this example:
// resume the generator after some time
setTimeout(() => generator.next(4), 2000);
In contrast with the regular functions, a generator and the calling code can change the results passing values in next/yield. (与常规函数相比,生成器和调用代码可以更改在next/yield中传递值的结果。)
Let’s see more calls in another example:
function* gen() {
let ask1 = yield "2 * 2 = ?";
console.log(ask1); // 4
let ask2 = yield "3 + 3 = ?";
console.log(ask2); // 6
}
let generator = gen();
console.log(generator.next().value); // "2 * 2 = ?"
console.log(generator.next(4).value); // "3 + 3 = ?"
console.log(generator.next(6).done); // true
The initial .next() starts the execution.It reaches the first yield. The outcome is returned to the outer code. The 2nd .next(4) passes 4 back to the generator just as the result of the initial yield, resuming the execution. Then it gets to the second yield, which becomes the result of the generator code. The 3rd next(6) passes 6 into the generator just as the result of the second yield resuming the execution that reaches the end of the function:done: true. Every next(value) (except the first one) passes a value into the generator, which becomes the result of the current yield, then gets back the result of the next yield.
Generator.throw
Generator.throw
As described above, the outer code can pass a value into the generator, as the result of yield. (如上所述,作为yield的结果,外部代码可以将值传递到生成器中。)
To make things more obvious, let’s consider the yield of “2 * 2 = ?”, which brings to an error:
function* gen() {
try {
let result = yield "2 * 2 = ?"; // (1)
console.log("The exception is thrown above, so the execution does not reach here");
} catch (e) {
console.log(e); // shows the error
}
}
let generator = gen();
let question = generator.next().value;
generator.throw(new Error("The answer isn't found in database")); // (2)
The error, which is thrown into the generator line (2), leads to an exception in line (1) with yield. The try..catch catches and shows it, in the example above. (该错误被抛入发电机管线( 2 ) ,导致在第( 1 )行出现异常,出现良率异常。try.. catch捕获并显示它,在上面的示例中。)
In case it is not caught, it will fall out the generator into the calling code. (如果没有被捕获,它将从生成器掉落到调用代码中。)
The calling code’s current line is the line with generator.throw, labeled as (2). So you can catch it here, as follows:
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 isn't found in database"));
} catch (err) {
console.log(err); // shows the error
}
If the error is not caught there, as a rule, it falls through to the outer calling code (if any), and if uncaught, destroys the script. (如果错误未被捕获,则通常会传递到外部调用代码(如果有) ,如果未捕获,则会销毁脚本。)
Summary
Summary (概要)
You can create generators with the help of generator functionsfunction* f(…) {…}. There is a unique yield operator inside generators.
The outer code and the generator can exchange results through next/yield calls. (外部代码和生成器可以通过next/yield调用来交换结果。)
Generators are not frequently used in JavaScript. But sometimes they are convenient, as the ability of a function to exchange data with the calling code during the execution is unique. And, finally, they work perfectly for making iterable objects. (JavaScript中不经常使用生成器。但有时它们很方便,因为函数在执行期间与调用代码交换数据的能力是独一无二的。最后,它们非常适合制作可迭代的对象。)