A generator is a special function that is defined using the function*
(note that *
) and yield
syntax, and exposes the next()
method. The difference between this and a normal function is that a generator function can be paused (using yield
) and resumed (using next()
). These functions are most commonly used to simplify iterators, however they are not limited to this.
Note: it is not possible to use arrow notation when defining a generator.
The yield
keyword returns an IteratorResult
object with two properties: value
and done
.
The next()
function can accept a single value (e.g. iter.next(5)
). This value is assigned as the output of the yield
keyword, regardless of what the true output is.
Simple Example
For example, in the most familiar case, an iterator function may be implemented in the following manner:
function* numMaker () {
let id = 0
while (true) { yield id++ }
}
// need to assign generator to a variable to make it iterable
let gen = numMaker()
This can then either be accessed using a for-of loop:
for (const i of numMaker()) {
if (i > 100)
break;
console.log(`id: ${i}`);
}
or the individual yielded values can be accessed sequentially using the next()
function:
gen.next().value // → 0
gen.next().value // → 1
gen.next().value // → 2
Multiple yield Statements
A possibly less familiar use case is to use multiple yield
statements in a function, to perform several tasks in sequence:
function* generator(e) {
yield e + 10;
yield e + 25;
yield e + 33;
}
var generate = generator(27);
console.log(generate.next().value); // 37
console.log(generate.next().value); // 52
console.log(generate.next().value); // 60
console.log(generate.next().value); // undefined
yield Sequencing
The sequence in which values are yielded from a generator can be confusing. Take the following example:
function* logGenerator() {
console.log(0);
console.log(1, yield 10);
console.log(2, yield 20);
console.log(3, yield 30);
}
var gen = logGenerator();
console.log("call 1 yielded", gen.next().value); // 0
console.log("call 2 yielded", gen.next('pretzel').value); // 1 pretzel
console.log("call 3 yielded", gen.next('california').value); // 2 california
console.log("call 4 yielded", gen.next('mayonnaise').value); // 3 mayonnaise
The output from this will be the following:
0
call 1 yielded 10
1 pretzel
call 2 yielded 20
2 california
call 3 yielded 30
3 mayonnaise
call 4 yielded undefined
The first time you call the generator it executes console.log(0);
and then starts executing console.log(1, yield 10)
. But when it gets to the yield
expression, next()
returns that value, before actually calling console.log()
.
The next time you call the generator, it resumes where it left off, which is constructing the arguments to console.log()
. The yield 10
expression is replaced with the argument to next()
, so it executes console.log(1, 'pretzel')
.
Then it starts executing console.log(2, yield 20)
, and the same thing happens – it yields 20 before calling console.log()
.
Emulating async/await
Generators can be used with Promises to emulate async/await functionality (actually, async/await uses similar logic under the hood). The following is a crude example of this:
shared code
// function to simulate a long-running process
function timeout(ms) {
return new Promise((resolve) => setTimeout(resolve.bind(null, "done!"), ms));
}
// returns a Promise that will return a 'random'
// number after 3 seconds
const getPromise = async () => {
// returns pending promise
await timeout(3000);
// returns when promise resolved
return Date.now();
};
const getData = (overriddenValue) => {
return overriddenValue ? Promise.resolve(overriddenValue) : getPromise();
};
const logData = (test, iter, data) => {
const timestamp = Date.now();
const delta = timestamp - data;
console.log(`${timestamp} - (${test}) Resolved Promise ${iter}: ${data} (delta: ${delta})`);
};
const timestamp = Date.now();
console.log(`${timestamp} - start`);
async/await version
// get Promise for expected data
// when Promise is resolved, use the resolved value.
getData().then((resolvedValue) => {
logData('await', "a", resolvedValue);
// get Promise for expected data
// when Promise is resolved, use the resolved value.
getData().then((resolvedValue) => {
logData('await', "b", resolvedValue);
// override expected data
// Promise will be instantly resolved.
const overriddenValue = resolvedValue + 100;
getData(overriddenValue).then((resolvedValue) => {
logData('await', "c", resolvedValue);
});
});
});
generator version
// incrementally returns Promises for each yield statement
// each time next() is called
function* generator() {
// first IterationResult: returns pending Promise
// for the expected data
let a = yield getData();
// second IterationResult: if a value was passed in via next,
// immediately return a resolved Promise with that value.
// If nothing was passed via next, return a pending Promise
// for the expected data
let b = yield getData(a);
// third IterationResult: if a value was passed in via next,
// immediately return a resolved Promise with that value.
// If nothing was passed via next, return a pending Promise
// for the expected data
yield getData(b);
}
// assign Generator to variable so that it can be iterated
const gen = generator();
// get Promise for expected data
// when Promise is resolved, use the resolved value.
gen.next().value.then((resolvedValue) => {
logData('yield', 1, resolvedValue);
// get Promise for expected data
// when Promise is resolved, use the resolved value.
gen.next().value.then((resolvedValue) => {
logData('yield', 2, resolvedValue);
// override expected data
// Promise will be instantly resolved.
const overriddenValue = resolvedValue + 10;
gen.next(overriddenValue).value.then((resolvedValue) => {
logData('yield', 3, resolvedValue);
});
});
});
yield*
The yield*
keyword can be used to embed a generator inside another generator:
function* g1() {
yield 2;
yield 3;
yield 4;
}
function* g2() {
yield 1;
yield* gl();
yield 5;
}
var iterator = g2();
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: 4, done: false}
console.log(iterator.next()); // {value: 5, done: false}
console.log(iterator.next()); // {value: undefined, done: true}
Further Information