This article brings you relevant knowledge about JavaScript. It mainly introduces the JavaScript front-end iterator and generator Generator. Friends in need can refer to it. I hope it will be helpful.
Front-end (vue) entry to mastery course: enter learning
Iterator provides a unified interface mechanism to provide a unified access mechanism for various different data structures.
Defining Iterator is to provide an object with a next() method. Each time next() is called, a result object will be returned. The result object has two attributes, value represents the current value, and done represents whether the traversal is completed.
function makeIterator(Array){ let index = 0; return { next: function(){ return ( Array.length > index ? {value: Array[index++]}: {done: true} ) } } } let iterator = makeIterator(['1','2']) console.log(iterator.next()); // {value: '1'} console.log(iterator.next()); // {value: '2'} console.log(iterator.next()); // {done: true}
The role of Iterator:
Provide a unified and simple access interface for various data structures;
Enables the members of a data structure to be arranged in a certain order;
for consumption by for...of
ES6 provides the for of statement to traverse the iterator object. We will use the for of statement to traverse the iterator created above:
let iterator = makeIterator(['1','2']) for (let value of iterator) { console.log(value); } // iterator is not iterable
The result is an error saying iterator is not iterable. Why is this? ES6 stipulates that the default Iterator interface is deployed in the Symbol.iterator property of the data structure. If a data structure has the Symbol.iterator property, the data structure can be traversed.
We transform the custom makeIterator as follows:
const MakeIterator = (Array) => ({ [Symbol.iterator](){ let index = 0; return { next(){ let length = Array.length; if(index < length){ return {value: Array[index++]} }else{ return {done: true} } } } } }) for(let value of MakeIterator([1,2])){ console.log(value) } // 1 // 2
We add a return method to MakeIterator. If the for...of loop exits early (usually because of an error or a break statement), the return() method will be called to terminate the traversal.
Based on this feature, if an object needs to clean up or release resources before completing the traversal, we can deploy the return() method and include closing the file when the file reading fails.
const MakeIterator = (Array) => ({ [Symbol.iterator](){ let index = 0; return { next(){ let length = Array.length; if(index < length){ return {value: Array[index++]} }else{ return {done: true} } }, return(){ return {done: true} } } } }) for(let value of MakeIterator([1, 2, 3])){ console.log(value) // 1 // Method 1 break; // Method 2 // throw new Error('error'); }
array
Set
Map
Array-like objects, such as arguments objects, DOM NodeList objects, typedArray objects
// arguments object function sum(){ for(let value of arguments){ console.log(value) } } sum(1,2) // 1 // 2 // typedArray object let typeArry = new Int8Array(2); typeArry[0] = 1; typeArry[1] = 2; for(let value of typeArry){ console.log(value) } // 1 // 2
Generator object
function*gen(){ yield 1; yield 2; } for(let value of gen()){ console.log(value) }
String
Q: Why doesn't Object have a native Iterator?
A: The reason why Object does not deploy the Iterator interface by default is because it is uncertain which property of the object is traversed first and which property is traversed later.
In essence, the traverser is a linear process. For any non-linear data structure, deploying the traverser interface is equivalent to deploying a linear transformation.
However, strictly speaking, the object deployment traverser interface is not necessary, because at this time the object is actually used as a Map structure. ES5 does not have a Map structure, but ES6 provides it natively.
Destructuring assignment
let set = new Set().add('a').add('b').add('c'); let [x,y] = set; // x='a'; y='b'
spread operator
var str = 'hello'; [...str] // ['h','e','l','l','o']
The spread operator calls the Iterator interface, so Object does not deploy the Iterator interface, so why can the... operator be used?
Reason: There are two types of spread operators
One is used in the case of function parameters and array expansion. In this case, the object is required to be iterable (iterable)
The other is for object expansion, that is, {...obj} form. In this case, the object needs to be enumerable (enumerable)
let obj1 = { name: 'qianxun' } let obj2 = { age: 3 } // The array object is enumerable let obj = {...obj1, ...obj2} console.log(obj) //{name: 'qianxun', age: 3} // Ordinary objects are not iterable by default let obj = [...obj1, ...obj2] console.log(obj) // object is not iterable
function forOf(obj, cb){ let iteratorValue = obj[Symbol.iterator](); let result = iteratorValue.next() while(!result.done){ cb(result.value) result = iteratorValue.next() } } forOf([1,2,3], (value)=>{ console.log(value) }) // 1 // 2 // 3
Conceptually
Generator function is an asynchronous programming solution provided by ES6. The Generator function is a state machine that encapsulates multiple internal states;
The Generator function is also a traverser object generation function, which returns a traverser object after execution.
formal
1. There is an asterisk between the function keyword and the function name;
2. Yield expressions are used inside the function body to define different internal states.
function* simpleGenerator(){ yield 1; yield 2; } simpleGenerator()
As above, we created a simple Generator, and we explored it with two questions:
What happens after the Generator function runs?
What does the yield expression in the function do?
function* simpleGenerator(){ console.log('hello world'); yield 1; yield 2; } let generator = simpleGenerator(); // simpleGenerator {<suspended}} console.log(generator.next()) // hello world // {value: 1, done: false} console.log(generator.next()) // {value: 2, done: false}
The Generator generator function returns a generator object after running, while the ordinary function will directly execute the code inside the function; each time the next method of the generator object is called, the function will be executed until the next yield keyword stops execution, and a {value: Value, done: Boolean} object.
The yield expression itself has no return value, or it always returns undefined. The next method can take one parameter, which will be treated as the return value of the previous yield expression. Through the parameters of the next method, different values can be injected from the outside into the inside at different stages of the Generator function to adjust the function behavior. Since the parameters of the next method represent the return value of the previous yield expression, passing parameters is invalid the first time the next method is used.
function sum(x){ return function(y){ return x + y; } } console.log(sum(1)(2)) // Use next parameter to rewrite function* sum(x){ let y = yield x; while(true){ y = yield x + y; } } let gen = sum(2) console.log(gen.next()) // 2 console.log(gen.next(1)) // 3 console.log(gen.next(2)) // 4
The role of yield expression: defining internal state and pausing execution. The difference between yield expression and return statement
The yield expression means that the function pauses execution and continues to execute backward from that position next time, while the return statement does not have the function of position memory.
In a function, only one return statement can be executed, but multiple yield expressions can be executed.
Any function can use the return statement. The yield expression can only be used in the Generator function. If used elsewhere, an error will be reported.
If the yield expression participates in the operation, put it in parentheses; if it is used as a function parameter or placed on the right side of the assignment expression, the parentheses may not be included.
function *gen () { console.log('hello' + yield) × console.log('hello' + (yield)) √ console.log('hello' + yield 1) × console.log('hello' + (yield 1)) √ foo(yield 1) √ const param = yield 2 √ }
Based on the fact that the Generator generator function can support multiple yields, we can implement a scenario where a function has multiple return values:
function* gen(num1, num2){ yield num1 + num2; yield num1 - num2; } let res = gen(2, 1); console.log(res.next()) // {value: 3, done: false} console.log(res.next()) // {value: 1, done: false}
Since the Generator function is the iterator generation function, the Generator can be assigned to the Symbol.iterator property of the object, so that the object has the Iterator interface. Generator implementation code is more concise.
let obj = { name: 'qianxun', age: 3, [Symbol.iterator]: function(){ let that = this; let keys = Object.keys(that) let index = 0; return { next: function(){ return index < keys.length? {value: that[keys[index++]], done: false}: {value: undefined, done: true} } } } } for(let value of obj){ console.log(value) }
Generator:
let obj = { name: 'qianxun', age: 3, [Symbol.iterator]: function* (){ let keys = Object.keys(this) for(let i=0; i< keys.length; i++){ yield this[keys[i]]; } } } for(let value of obj){ console.log(value) }
The
return()
method can return the given value and terminate the traversal Generator function.
function*gen() { yield 1; yield 2; yield 3; } var g = gen(); g.next() // { value: 1, done: false } // If no parameters are provided when the return() method is called, the value attribute of the return value is undefined g.return('foo') // { value: "foo", done: true } g.next() // { value: undefined, done: true }
If there is try...finally
code block inside the Generator function and the try
code block is being executed, then return()
method will cause finally
code block to be entered immediately. After execution, the entire function will end.
function* numbers () { yield 1; try { yield 2; yield 3; } finally { yield 4; yield 5; } yield 6; } var g = numbers(); g.next() // { value: 1, done: false } g.next() // { value: 2, done: false } g.return(7) // { value: 4, done: false } g.next() // { value: 5, done: false } g.next() // { value: 7, done: true }
If you want to call another Generator function inside the Generator function. We need to manually complete the traversal within the function body of the former. If the function call is nested at multiple levels, the writing will be cumbersome and difficult to read. ES6 provides yield* expressions as a solution.
Delegate to other generators
function* g1() { yield 2; yield 3; } function* g2() { yield 1; yield* g1(); yield 4; } const 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: undefined, done: true }
Delegate to other iterable objects
function*gen(){ yield* [1,2,3] } console.log(gen().next()) // {value: 1, done: false}
The Generator function returns a traverser. ES6 stipulates that this traverser is an instance of the Generator function and inherits the methods on the Generator.prototype object, but cannot obtain the properties on this because this is the global object at this time, not the instance object.
function*gen(){ this.a = 1 } gen.prototype.say = function(){ console.log('hi') } let obj = gen() console.log(obj instanceof gen) // true obj.say() // hi obj.next() console.log(obj.a) //undefined
If you want to access instance properties like a constructor, you can modify this to bind it to Generator.prototype.
function*gen(){ this.a = 1 } gen.prototype.say = function(){ console.log('hi') } let obj = gen.call(gen.prototype) console.log(obj instanceof gen) // true obj.say() // hi obj.next() console.log(obj.a) //1
function* StateMachine(state){ let transition; while(true){ if(transition === "INCREMENT"){ state++; }else if(transition === "DECREMENT"){ state--; } transition = yield state; } } const iterator = StateMachine(0); console.log(iterator.next()); // 0 console.log(iterator.next('INCREMENT')); // 1 console.log(iterator.next('DECREMENT')); // 0