Preface
Actually, previously summarized in [Ayqy: JavaScript Generator](/articles/javascript 生成器/), this is not intentionally repeating. Previously was summary based on MDN documentation, focusing on syntax rules, this article according to firsthand account from implementer of this feature in Firefox, supplements some details and application scenarios
To avoid repetition, this article no longer explains syntax rules (function* + yield), for syntax details please see previous article
1. Function and Internal Principle
generator (generator) is used to create iterators, syntax is very concise (function* + yield)
When generator executes yield statement, generator's stack structure (local variables, parameters, temporary values, generator's internal current execution position) is removed from stack. But generator object retains reference to this stack structure (backup), so later calling .next() can reactivate stack structure and continue execution
For example:
// Define generator
var gen = function*() {
console.log('before yield 1');
yield 1;
console.log('before yield 2');
yield 2;
}
// Call generator returns iterator
var iter = gen();
iter.next(); // before yield 1
// Object {value: 1, done: false}
iter.next(); // before yield 2
// Object {value: 2, done: false}
iter.next(); // Object {value: undefined, done: true}
iter.next(); // Object {value: undefined, done: true}
yield statement divides function body into several segments, .next() executes one segment at a time
2. Iterators and Generators
What
function*defines is called iterator's generator (abbreviated as generator), because calling it returns an iterator
All generators have built-in implementations of .next() and [Symbol.iterator]() methods, we only need to write loop part's behavior. Function body after function* is like loop body of loop structure, for example:
function* gen(arr) {
for (var i = 0; i < arr.length; i++) {
yield arr[i];
}
}
var iter = gen([1, 2, 4]);
console.log(iter.next()); // Object {value: 1, done: false}
console.log(iter.next()); // Object {value: 2, done: false}
Among them gen's function is to turn continuous array into "breathing" array, for loop originally can't stop, but yield indeed made it stop, this is also one of generator's major features. Using this feature can implement many interesting things, such as using animation to display quicksort process, pseudo code as follows:
function quicksort(arr) {
// sort
forloop {
updateSortedArr(); // Complete one pass of sorting
displaySortedArr(); // Display this pass's sorting result
}
return sortedArr;
}
Of course, this way cannot see animation. Because n passes of sorting complete instantly (too fast, can't see anything clearly where's the animation, didn't change at all). Easy to think of workaround: first store each pass's result, finally display:
function quicksort(arr) {
var tmpArr = [];
forloop {
updateSortedArr(); // Complete one pass of sorting
tmpArr.push(sortedArr); // Store it
}
// Animation display sorting process
anim(tmpArr);
}
After getting each pass's result, can display however want. Feels not very difficult, okay, if want to animate display movement of 2 pointers in each pass of quicksort? We seem to need to record pointer movement's tmpArr again, if this array is very large, if... This will inevitably need more memory space to record already happened things
Think carefully, during sorting process, data needed for animation display already exists, so can we sort while animating display? Of course can:
function* quicksort(arr) {
forloop {
yield sortedArr;
// Or
// yield step;
}
}
var iter = quicksort(arr);
// Animation display sorting process
function anim() {
display(iter.next());
setTimeout(anim, 300);
}
anim();
Yes, generator makes loop able to "breathe", create animation during breathing process
P.S. Actually even without generator, we can also make loop "stop", please see [Ayqy: JavaScript Implement yield](/articles/javascript 实现 yield/)
P.S. For specific details of quicksort, please see Ayqy: Sorting Algorithm Quicksort Analysis
3. Characteristics
Generator's characteristics are as follows:
-
Ordinary functions cannot self-pause, generator functions can
-
yieldonly valid in direct scope offunction*,yieldin anonymous function insidefunction*is illegal -
Can handle infinite sequences. Cannot construct infinitely large arrays, but can use generator to implement construction rules of infinite sequences, to handle infinite sequences
-
Provides another idea for returning arrays, return generator instead of array, trade time for space
-
Can refactor complex loops, split it into 2 parts, convert data generation part to generator, then for...of iterate through these data
-
Can quickly create iterators, make any object iterable, specifically please see [Ayqy: for…of Loop_ES6 Notes 1](/articles/for-of 循环-es6 笔记 1/)
-
Easy to extend iterators, very easy to implement filtering etc. operations
Point 2 needs attention, because most materials introducing generators won't mention this point, but we indeed cannot setTimeout delay yield in generator. Using generator to extend iterator is a good choice, code is very natural, example as follows:
// Extend iterator
function* filter(isValid, iterable) {
for (var val of iterable) {
if (isValid(val)) {
yield val;
}
}
}
// test
function isValid(val) {
return val > 1;
}
for (var val of filter(isValid, [0, 1, 2, 4])) {
console.log(val);
}
Wrapping iterator with generator, has a kind of seamless connection beauty, doesn't it?
4. Advanced Techniques
1. Affect Generator's Logic Flow from Outside
Iterator's next(returnVal) method accepts optional parameter, parameter will be used as return value of previous yield statement in generator, this way caller can affect generator's logic flow from outside, for example:
function* gen() {
var water = yield 'give me a cup of pure water';
yield water.drink();
}
// test
var iter = gen();
console.log(iter.next());
console.log(iter.next({
name: 'pure water',
drink: function() {
return 'hmm, well';
}
}));
First yield asks caller for a cup of water, second .next() hands water to generator, then second yield drinks the water
This is a two-way interaction process, in practical applications, judge what generator needs according to next()'s return value, then pass it through next .next(), isolate complex logic in generator, calling is as easy as conversation
2. Terminate Iterator
Note, it's iterator, there are two methods to terminate an iterator:
-
Iterator's
throw(err)method, effect is like yield expression in generator calls a function and throws error -
Iterator's
return(returnVal)method, accepts optional parameter, parameter will be returned as value (doneistrue), generator only executesfinallycode block and no longer resumes execution
throw example as follows:
// throw
function* gen() {
try {
yield 1;
yield 2;
} catch (err) {
console.log('error occurs: ' + err);
} finally {
console.log('clean up');
}
}
var iter = gen();
console.log(iter.next()); // Object {value: 1, done: false}
console.log(iter.throw(new Error('err'))); // error occurs: Error: err
// clean up
// Object { value: undefined, done: true }
console.log(iter.next()); // Object {value: undefined, done: true}
throw(err) indicates iterator abnormally closes, used to notify generator internal to execute cleanup work in finally
While return() indicates iterator normally closes, example as follows:
console.log(iter.next()); // Object {value: 1, done: false}
console.log(iter.return('ok')); // clean up
// Object { value: "ok", done: true }
console.log(iter.next()); // Object {value: undefined, done: true}
Note: 'ok' as value returns immediately, not returned at next .next()
P.S. Chrome49 still doesn't support return(), FF throw() after still can return(), but if first return() then throw() will error
3. Splice Iterators
yield* iter can splice iterators, supports calling another generator in one generator, for example:
var gen1 = function* (){
yield 1;
yield 2;
}
var gen2 = function* (){
yield* gen1();
yield 3;
yield 4;
}
for (var val of gen2()) {
console.log(val); // 1 2 3 4
}
5. Summary
Generator can make execution flow "breathe", can make things that can't stop pause, can be used to refactor loops, can control infinite sequences, can wrap iterators... many benefits
References
- "ES6 in Depth": Free e-book provided by InfoQ Chinese site
No comments yet. Be the first to share your thoughts.