- What are JavaScript Generators and how to use them
- Original author: Vladislav Stepanov
- The Nuggets translation Project
- Permanent link to this article: github.com/xitu/gold-m…
- Translator: lsvih
- Proofread by: Zhongdeming428, old professor
In this article, we’ll look at generators introduced in ECMAScript 6. Take a look at what it is, then use a few examples to illustrate how it can be used.
What is a JavaScript generator?
A generator is a function that can be used to control an iterator. It can be paused at any time and resumed at any time.
The above description doesn’t explain much, so let’s look at some examples of what generators are and how they differ from iterators like for loops.
Here is an example of a for loop that returns some values immediately after execution. This code simply generates the numbers 0-5.
for (leti = 0; i < 5; i += 1) { console.log(i); } // It will immediately return 0 -> 1 -> 2 -> 3 -> 4Copy the code
Now look at the generator function.
function * generatorForLoop(num) {
for (leti = 0; i < num; i += 1) { yield console.log(i); } } const genForLoop = generatorForLoop(5); genForLoop.next(); // console.log-0 genforloop.next (); // 1 genForLoop.next(); // 2 genForLoop.next(); // 3 genForLoop.next(); / / 4Copy the code
What did it do? It really just changes the for loop in the example above a bit, but it makes a big difference. This variation is caused by the most important feature of the generator — it produces the next value only when it is needed, not all at once. This feature can be handy in some situations.
Generator syntax
How do you define a generator function? The following lists the possible definitions, but the most common one is to put an asterisk after the function keyword.
function * generator () {}
function* generator () {}
function *generator () {}
let generator = function * () {}
let generator = function* () {}
let generator = function* () {}let generator = *() => {} // SyntaxError
let generator = ()* => {} // SyntaxError
let generator = (*) => {} // SyntaxError
Copy the code
As the example above shows, we can’t use the arrow function to create a generator.
Let’s create a generator as a method. Methods are defined in the same way as functions.
class MyClass {
*generator() {}
* generator() {}
}
const obj = {
*generator() {}
* generator() {}}Copy the code
yield
Now let’s take a look at the new keyword yield. It is somewhat similar to return, but not exactly the same. The return simply returns the value after the function call. You can’t do anything after the return statement.
function withReturn(a) {
let b = 5;
returna + b; b = 6; // It is impossible to redefine breturna * b; } withReturn(6); // 11 withReturn(6); / / 11Copy the code
Yield works differently.
function * withYield(a) {
letb = 5; yield a + b; b = 6; // The variable yield a * b can be redefined after the first call; } const calcSix = withYield(6); calcSix.next().value; // 11 calcSix.next().value; / / 36Copy the code
A value returned by yield is returned only once. When you call the same function again, it is executed to the next yield statement.
In generators, we usually get an object on output. This object has two properties: value and done. As you might expect, value is the return value, and done shows whether the generator has finished its work.
function * generator() {
yield 5;
}
const gen = generator();
gen.next(); // {value: 5, done: false}
gen.next(); // {value: undefined, done: true}
gen.next(); // {value: undefined, done: true} - Any subsequent calls will return the same resultCopy the code
In generators, you can use both yield and return to return the same object. However, when the function executes to the first return statement, the generator finishes its work.
function * generator() {
yield 1;
return2; yield 3; } const gen = generator(); gen.next(); // {value: 1,done: false}
gen.next(); // {value: 2, done: true}
gen.next(); // {value: undefined, done: true}
Copy the code
Yield delegate iteration
Yield with an asterisk can delegate its work to another generator. In this way, you can link multiple generators together.
function * anotherGenerator(i) {
yield i + 1;
yield i + 2;
yield i + 3;
}
function* generator(i) { yield* anotherGenerator(i); } var gen = generator(1); gen.next().value; // 2 gen.next().value; // 3 gen.next().value; / / 4Copy the code
Before we move on to the next section, let’s look at a behavior that at first glance seems odd.
The following is normal code, which does not report any errors, indicating that yield can return the value passed after the next() method is called:
function * generator(arr) {
for (const i inarr) { yield i; yield yield; yield(yield); }} const gen = generator([0,1]); gen.next(); // {value:"0".done: false}
gen.next('A'); // {value: undefined, done: false}
gen.next('A'); // {value: "A".done: false}
gen.next('A'); // {value: undefined, done: false}
gen.next('A'); // {value: "A".done: false}
gen.next(); // {value: "1".done: false}
gen.next('B'); // {value: undefined, done: false}
gen.next('B'); // {value: "B".done: false}
gen.next('B'); // {value: undefined, done: false}
gen.next('B'); // {value: "B".done: false}
gen.next(); // {value: undefined, done: true}
Copy the code
In this example, you can see that yield is undefined by default, but if we pass any value when we call yield, it will return the value we passed in. We’ll take advantage of this feature soon.
Initialization and methods
Generators can be reused, but you need to initialize them. Fortunately, the initialization method is very simple.
function * generator(arg = 'Nothing') {
yield arg;
}
const gen0 = generator(); // OK
const gen1 = generator('Hello'); // OK const gen2 = new generator(); / / not OK the generator (). The next (); // It can be run, but will be run from scratch each timeCopy the code
As shown above, gen0 and gen1 do not interact, and Gen2 does not run at all. Therefore, initialization is very important to ensure the state of the program flow.
Let’s take a look at the methods provided by the generator.
Next () method
function * generator() {
yield 1;
yield 2;
yield 3;
}
const gen = generator();
gen.next(); // {value: 1, done: false}
gen.next(); // {value: 2, done: false}
gen.next(); // {value: 3, done: false}
gen.next(); // {value: undefined, done: true} all next calls after that return the same outputCopy the code
This is the most common method. It returns the next object every time it is called. At the end of the generator’s work, next() sets the done attribute to true and the value attribute to undefined.
Not only can we iterate over the generator with next(), but we can also use the for of loop to get all of the generator’s values (instead of the objects) at once.
function * generator(arr) {
for (const el in arr)
yield el;
}
const gen = generator([0, 1, 2]);
for (const g of gen) {
console.log(g); // 0 -> 1 -> 2
}
gen.next(); // {value: undefined, done: true}
Copy the code
It does not work with for-in loops, however, and attributes cannot be accessed directly with numeric subscripts: Generator [0] = undefined.
Return () method
function * generator() {
yield 1;
yield 2;
yield 3;
}
const gen = generator();
gen.return(); // {value: undefined, done: true}
gen.return('Heeyyaa'); // {value: "Heeyyaa".done: true}
gen.next(); // {value: undefined, done: true} -returnAll next() calls after () return the same outputCopy the code
Return () will ignore any code in the generator. It sets value based on the passed value and sets done to true. Any next() call made after return() returns an object with the done attribute true.
Throw () method
function * generator() {
yield 1;
yield 2;
yield 3;
}
const gen = generator();
gen.throw('Something bad'); // Error Uncaught Something bad gen. Next (); // {value: undefined,done: true}
Copy the code
What throw() does is very simple — it throws an error. We can do try-catch.
Implementation of custom methods
Since we do not have direct access to Generator constructor, how to add new methods needs to be explained separately. Here’s how I did it, and you can do it differently:
function * generator() { yield 1; } generator.prototype.__proto__; // Generator {constructor: ƒ, next: ƒ,returnƒ, throw: ƒ, Symbol(symbol.tostringtag):"Generator"} // Since Generator is not a global variable, we can only write generator.prototype.__proto__. Math =function(e = 0) {
returne * Math.PI; } generator.prototype.__proto__; // Generator {constructor: ƒ, constructor: Generator, next: ƒ,returnƒ } const gen = generator(); gen.math(1); / / 3.141592653589793Copy the code
Purpose of generators
Previously, we used a generator with a known number of iterations. But what if we don’t know how many iterations we have to iterate? To solve this problem, you need to create an infinite loop in the generator function. Here is an example of a function that returns a random number:
function* randomFrom(... arr) {while (true) yield arr[Math.floor(Math.random() * arr.length)]; } const getRandom = randomFrom(1, 2, 5, 9, 4); getRandom.next().value; // Return a random numberCopy the code
This is a simple example. For more complex functions, we’ll write a throttle-down function. If you don’t know what a throttling function is, please refer to this article.
function * throttle(func, time) {
let timerID = null;
function throttled(arg) {
clearTimeout(timerID);
timerID = setTimeout(func.bind(window, arg), time);
}
while (true)
throttled(yield);
}
const thr = throttle(console.log, 1000);
thr.next(); // {value: undefined, done: false}
thr.next('hello'); // return {value: undefined,done: false}, and output 1 second later'hello'
Copy the code
Is there a better use of generators? If you know recursion, you’ve probably heard of Fibonacci numbers. Normally we solve this problem recursively, but with generators we can write:
function * fibonacci(seed1, seed2) {
while (true) {
yield (() => {
seed2 = seed2 + seed1;
seed1 = seed2 - seed1;
returnseed2; }) (); } } const fib = fibonacci(0, 1); fib.next(); // {value: 1,done: false}
fib.next(); // {value: 2, done: false}
fib.next(); // {value: 3, done: false}
fib.next(); // {value: 5, done: false}
fib.next(); // {value: 8, done: false}
Copy the code
No more recursion! We can get the next number in the sequence when we need it.
Use the generator on HTML
Since we’re talking about JavaScript, it’s obvious to use generators to manipulate HTML.
Assuming you have some HTML blocks to work with, you can easily do this using a generator. (There are more ways to do this than generators, of course.)
We can do this with a little code.
const strings = document.querySelectorAll('.string');
const btn = document.querySelector('#btn');
const className = 'darker';
function * addClassToEach(elements, className) {
for (const el of Array.from(elements))
yield el.classList.add(className);
}
const addClassToStrings = addClassToEach(strings, className);
btn.addEventListener('click', (el) => {
if (addClassToStrings.next().done)
el.target.classList.add(className);
});
Copy the code
Only five lines of logic.
conclusion
There are more ways to use generators. Generators are also useful for asynchronous operations or on-demand loops, for example.
I hope this article has given you a better understanding of JavaScript generators.
The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.