Iterator与Generator学习笔记

本文阅读资料来自阮一峰ES6入门自己仅仅根据自己需要整理了自己所阅读的笔记

Iterator

Iterator就是一个遍历器,可以手动控制遍历.在js中for循环都是直接遍历的,如果我们想手动控制遍历还得自己写函数包装.
阮一峰的Blog给出了一个模拟Iterator方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array){
var nextIndex = 0;
return {
next: function(){
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
}
}

数组与对象的Iterator接口

数组原生就带了Iterator遍历器

1
2
3
4
let arr=["a","b","c"]
let iter=arr[Symbol.iterator]()
iter.next()// { value: 'a', done: false }
....

在对象中,因为对象的遍历属性是不确定的,所以对象并没有部署Iterator接口. 只能在构造函数中去定义遍历器.
定义遍历器的方法也很简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let obj = {
data: [ 'hello', 'world' ],
[Symbol.iterator]() {
const self = this;
let index = 0;
return {
next() {
if (index < self.data.length) {
return {
value: self.data[index++],
done: false
};
} else {
return { value: undefined, done: true };
}
}
};
}
};

这段代码很明显.遍历器仅仅是遍历数组而不是遍历对象.另外如果一个对象类似于数组那么我们可以直接把数组的遍历器给挂载上去

1
2
3
4
5
6
7
8
9
10
let iterable = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
console.log(item); // 'a', 'b', 'c'
}

普通对象当然不行的.那么对象就没办法遍历了? 其实可以用Object.keys(obj)来取出key值 这个key值是一个数组 这时候就可以遍历了

1
2
3
4
5
6
7
8
let obj={
"s":1,
"ss":2,
"sss":3
}
let keys=Object.keys(obj);
let objIterator=keys[Symbol.iterator]()
console.log(objIterator.next());//{value:"s",done:"false"}

for of循环

只要部署了遍历器那么就可以使用for of语句

数组

1
2
3
4
let arr1=["red","green","blue"];
for (let arr1Value of arr1){
console.log(arr1Value)//"red","green","blue"
}

这里是直接获取到数组的value了,如果要获取数组的索引那么可以调用数组的keys方法或者实例方法(PS:NODEJS 5.9不支持解构 等6.0吧 )

1
2
3
4
5
6
7
let arr2=["red","green","blue"];
for ( let [arr2Index,arr2Value] of arr2.entries()){
console.log(arr2Index,arr2Value)
//0 red
//1 green
//2 blue
}

如果一个对象类似数组(具有length,键为Number),可以直接用Array.from转为数组

对象

对象因为不会部署遍历器,所以可以用Object.keys获取键名后再遍历

1
2
3
for (var key of Object.keys(someObject)) {
console.log(key + ": " + someObject[key]);
}

或者用Generator函数包装一下

1
2
3
4
5
6
7
8
9
function* entries(obj){
for(let key of Object.keys(obj)){
yield [key,obj[key]]
}
}
for(let objValue of entries(obj)){
console.log(objValue)//[key,value]
}

for of 的优点

  • 可以配合break和return命令跳出循环
  • for in 遍历键值属性是不确定的

Generator函数

Generator函数的定义就是在function后面加一个*号 并且Generator函数内部可以使用yield关键字
从语法上,首先可以把它理解成,Generator函数是一个状态机,封装了多个内部状态。
执行Generator函数会返回一个遍历器对象(Iterator Object),也就是说,Generator函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历Generator函数内部的每一个状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function* helloWorldGenerator(){
yield "hello";
yield "world";
return "ending";//结束执行
}
let hw=helloWorldGenerator()
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }

上面调用了4次next方法.

  • 第一次调用遇到第一个yield语句 停止执行同时返回yield后面的表达式的值..
  • 第二次调用继续执行遇到第二个yield语句..
  • 第三次执行的时候遇到了return语句 而return语句下并没有yield关键字了 于是value返回return后面的关键词,done设置为true.如果没有return语句那么value将返回undefined,done仍旧为true
  • 第四次执行 因为generator函数已经运行完毕 以后再调用这个方法value都会返回undefined done为true

yield语句

遍历器对象的next逻辑运行如下

  • 遇到yield语句,暂停执行后面操作,并将紧跟在yield后面的那个表达式的值作为返回对象的value属性值
  • 下一次调用next继续往下执行,直到遇到下一个yield语句
  • 如果没有再遇到新的yield语句,那么就一直运行到函数结束,直到return语句位置 同时将return后面表达式的值作为value返回
  • 如果函数没有return语句,那么则返回对象的value属性值为undefined
1
2
3
function* gen(){
yield 123+456
}

上述yield后面的表达式不会立即求值,只会在next方法将指针移到这一句时才会求值.
如果generator不用yield语句,这时就变成了一个单纯的暂缓执行函数

1
2
3
4
5
function* f(){
console.log("执行了!")
}
var generator1=f();
setTimeout(()=>{generator1.next()},2000)

上述将会在2秒之后输出’执行了’ 那么把setTimeout改为setInterval会怎么样?

1
setInterval(()=>{console.log(generator1.next())},2000);

那么第一次将会输出

1
2
执行了!
{ value: undefined, done: true }

随后只会输出

1
{ value: undefined, done: true }

当然yield语句无法用在普通的函数中,forEach中也不行,只能用在generator函数中.但是for of与for语句是可以的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var arr3=[1,[[2,3],4,[5,6]]]
var flat=function* (a){
var length=a.length;
for(var i=0;i<length;i++){
var item=a[i];
if(typeof item !== "number"){
yield* flat(item);
}else{
yield item
}
}
}
for (var f of flat(arr3)) {
console.log(f);
}
// 1, 2, 3, 4, 5, 6

yield* 函数会在下述写笔记

与Iterator接口的关系

任意一个对象的Symboliterator方法,等于该对象的遍历器对象生成函数,调用该函数会返回该对象的一个遍历器对象。
遍历器对象本身也有Symbol.iterator方法,执行后返回自身。

1
2
3
4
5
6
7
8
function* gen(){
// some code
}
var g = gen();
g[Symbol.iterator]() === g
// true

上面代码中,gen是一个Generator函数,调用它会生成一个遍历器对象g。它的Symbol.iterator属性,也是一个遍历器对象生成函数,执行后返回它自己。

next方法的参数

yield语句本身没有返回值或者说总是返回undefined,next方法可以带一个参数,该参数就会被当做上一个yield语句的返回值

1
2
3
4
5
6
7
8
9
10
11
12
function* f1(){
for(let i=0;true;i++){
var reset=yield i;
if(reset){
i=-1;
}
}
}
var g1=f1();
g1.next();//{value:0,done:false}
g1.next(true );//{value:1,done:false}
g1.next();//{value:0,done:false}

这样理解,yield后面的表达式返回的是value值,但是如果在next方法传递值的时候reset始终是undefined
因为是for循环所以会生成无限个yield语句.那么每一次执行next都会返回一个i 那么在执行next()的时候reset等于next传递的值,那么就可以动态改变generator函数内部的值了
再看一个例子

1
2
3
4
5
6
7
8
9
function* foo(x){
var y=2*(yield(x+1));
var z=yield(y/3);
return (x+y+z);
}
var a=foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

来分析下,首先生成一个generator对象a并且传递5进去 那么在generator函数内部x就等于5在调用第一个next的时候会指向(x+1)=6 那么yield后面的表达式值为6所以第一次执行value为6.当然next仅仅是执行yield后面的语句这时候会暂停 并不会执行2(yield(x+1))
第二次调用next的时候会执行2
undefined并且赋值为y value会返回undefined/3 当然是NaN
第三次调用 遇到return语句 done为true value为6+undefined+undefined 那么自然是NaN
再根据这个例子写一个generator对象

1
2
3
4
var b=foo(5);
b.next();//{value:6,done:false}
b.next(12);//{value:8,done:false}
b.next(13);//{value:42,done:true}

第一次调用next x为5 同时暂停执行var y=2(…) 始终记住next仅仅是执行yield就暂停了
那么第二次调用next 开始执行上述的var y=2
(12)=24 然后接着执行24/3=8 那么value就为8
第三次调用next 开始指向上述var z=13 同时接着往下执行return语句 最终结果是(5+24+13)
阮一峰Blog是这样解释的.但是上述的会方便个人理解

注意,由于next方法的参数表示上一个yield语句的返回值,所以第一次使用next方法时,不能带有参数。V8引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。

generator与for of循环

for of循环可以自动遍历generator所以不需要调用next方法

1
2
3
4
5
6
7
8
9
10
11
12
function* foo1(){
yield 1;
yield 2;
yield 3;
return 4;
}
for(let v of foo1()){
console.log(v);
// 1
// 2
// 3
}

不会返回4 是因为for of循环检测到done为true的时候就不会返回了 当然done为true的时候其中value也不会返回
下面是一个利用Generator函数和for…of循环,实现斐波那契数列的例子。

1
2
3
4
5
6
7
8
9
10
11
12
function* fibonacci() {
let [prev, curr] = [0, 1];
for (;;) {
[prev, curr] = [curr, prev + curr];
yield curr;
}
}
for (let n of fibonacci()) {
if (n > 1000) break;
console.log(n);
}

看来在generator函数中 死循环也是没问题了 哈哈哈 while(true)也是没问题的

for…of循环、扩展运算符(…)、解构赋值和Array.from方法内部调用的,都是遍历器接口。这意味着,它们可以将Generator函数返回的Iterator对象,作为参数。

1
2
3
4
5
6
7
8
9
10
11
12
function* numbers(){
yield 1
yield 2
return 3
yield 4
}
[...numbers()]//[1,2]
Array.from(numbers())//[1,2]
let [x,y]=numbers();//x:1 y:2
for (let n of numbers()){
console.log(n) // 1 2
}

对象无法使用for of遍历 那么我们有两种方法加上generator后使他可以遍历

1.generator包装

1
2
3
4
5
6
7
8
9
10
function* objectEntries(obj){
let propKeys=Reflect.owKeys(obj);//Reflect是Symbol的新语法 详情可以访问Symbol
for(let propKey of propKeys){
yield [propKey,obj[propKey]]
}
}
let jane={first:"Jane",last:"Doe"}
for(let [key,value] of objectEntries(jane)){
console.log(`${key}:${value}`)
}

2.挂载到对象[Symbol.iterator]属性上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* objectEntries() {
let propKeys = Object.keys(this);
for (let propKey of propKeys) {
yield [propKey, this[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };
jane[Symbol.iterator] = objectEntries;
for (let [key, value] of jane) {
console.log(`${key}: ${value}`);
}

Generator.prototype.throw()

Generator的错误捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var g = function* () {
try {
yield;
} catch (e) {
console.log('内部捕获', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b

第一个错误首先被内部的try catch语句捕获 第二次调用next的时候因为内部已经捕获了 所以会暴露到外部 所以会被外部的捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var g = function* () {
while (true) {
try {
yield;
} catch (e) {
if (e != 'a') throw e;
console.log('内部捕获', e);
}
}
};
var i = g();
i.next();
try {
throw new Error('a');
throw new Error('b');}
} catch (e) {
console.log('外部捕获', e);
}
// 外部捕获 [Error: a]

因为这里在执行try的时候 其中已经已经爆出了Error(“a”)就直接进入catch语句内部 并不会继续执行Error(“b”)了

1
2
3
4
try {
throw new Error('a');
throw new Error('b');
}

如果generator函数内部没有部署try…catch代码块那么爆出的错误会被外部的所捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var g = function* () {
while (true) {
yield;
console.log('内部捕获', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
// 外部捕获 a

如果generator函数内部部署了try代码块 在使用遍历器的throw方法报错后并不会影响下一次遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var gen = function* gen(){
try {
yield console.log('hello');
} catch (e) {
// ...
}
yield console.log('world');
}
var g = gen();
g.next();
g.throw();
g.next();
// hello
// world

简单总结就是以几点

  • generator函数有个throw方法 使用这个throw方法如果遍历器内部部署了try catch代码则会被内部的处理 并且不影响下一次遍历.
  • 如果没有部署try catch那么会被外部的try catch语句获取并且处理 并且遍历终止.
  • 外部的全局的throw Error()方法只会被外部的trycatch捕获 并且不会影响遍历器内部遍历状态.
  • 遍历器内部爆出的错误可以被外部try catch捕获到

Generator.prototype.return()

很简单,立马结束遍历 并且返回return(arg)中的参数值

1
2
3
4
5
6
7
8
9
10
11
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }

yield*语句

yield*可以在generator函数内部调用另一个generator函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
function* foo() {
yield 'a';
yield 'b';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "a"
// "b"
// "y"

再来看一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
function* inner(){
yield "hello"
}
function* outer1(){
yield "open";
yield inner();
yield "close"
}
var gen=outer1()
gen.next().value //'open'
gen.next().value //'返回一个遍历器(inner的)对象'
gen.next().value //'close'

单纯的yield后面跟一个generator仅仅是返回他的对象 并不会影响next下一步的行动

1
2
3
4
5
6
7
8
9
function* outer2(){
yield 'open'
yield* inner()
yield "close"
}
var gen = outer2()
gen.next().value // "open"
gen.next().value // "hello!"
gen.next().value // "close"

yield则可以认为控制权被yield后面的generator函数所拿去了当yield后面的generator函数执行完毕后再继续执行原generator函数
从语法角度看,如果yield命令后面跟的是一个遍历器对象,需要在yield命令后面加上星号,表明它返回的是一个遍历器对象。这被称为yield
语句。
如果被代理的Generator函数有return语句,那么就可以向代理它的Generator函数返回数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function *foo() {
yield 2;
yield 3;
return "foo";
}
function *bar() {
yield 1;
var v = yield *foo();
console.log( "v: " + v );
yield 4;
}
var it = bar();
it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}

再来看个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
function* genFuncWithReturn() {
yield 'a';
yield 'b';
return 'The result';
}
function* logReturned(genObj) {
let result = yield* genObj;
console.log(result);
}
[...logReturned(genFuncWithReturn())]
// The result
// 值为 [ 'a', 'b' ]

首先执行logReturned(genFuncWithReturn())
那么在logReturned内部 首先执行yield 这里会返回两个值[a,b]同时会给result赋值上’The result’ 于是再执行解构运算符[…[a,b]]得出的结果是[a,b]
再来看个用yield
取出嵌套数组的所有成员

1
2
3
4
5
6
7
8
9
10
11
12
13
function* iterTree(tree){
if(Array.isArray(tree)){
for(let i=0;i<tree.length;i++){
yield* iterTree(tree[i])
}else{
yield tree;
}
}
}
const tree=['a',['b','c'],['d','e']];
for(let x of iterTree(tree)){
console.log(x)
}

首先iterTree会遍历所有的数组 并且会返回相应的值.类似于嵌套for循环

Generator函数的this

Generator函数总是返回一个遍历器,ES6规定这个遍历器是Generator函数的实例,也继承了Generator函数的prototype对象上的方法。

1
2
3
4
5
6
7
8
9
10
function* g() {}
g.prototype.hello = function () {
return 'hi!';
};
let obj = g();
obj instanceof g // true
obj.hello() // 'hi!'

g返回的总是遍历器对象而不是g本身的对象.如下

1
2
3
4
5
6
function* g() {
this.a = 11;
}
let obj = g();
obj.a // undefined

Generator与状态机

1
2
3
4
5
6
7
8
var ticking = true;
var clock = function() {
if (ticking)
console.log('Tick!');
else
console.log('Tock!');
ticking = !ticking;
}

用generator实现如下

1
2
3
4
5
6
7
8
var clock = function*() {
while (true) {
console.log('Tick!');
yield;
console.log('Tock!');
yield;
}
};

Generator处理异步

处理ajax

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function* main(){
var result=yield request("http://some.url");
var resp=JSON.parse(result);
console.log(resp.value)
}
function request(url){
ajax(url,function(response){
it.next(response)
})
}
var it=main();
it.next()

首先执行的it.next()仅仅是执行yield后面的request,在request中等到有返回结果了再执行下一步yield 这时候就会把next中的response赋值给result 那么最后resp拿到的就是ajax返回后的结果

控制流管理

如果有一个多步操作非常耗时,采用回调函数,可能会写成下面这样。

1
2
3
4
5
6
7
8
9
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
});
});
});
});

用promise改写上面的代码

1
2
3
4
5
6
7
8
9
10
Q.fcall(step1)
.then(step2)
.then(step3)
.then(step4)
.then(function (value4) {
// Do something with value4
}, function (error) {
// Handle any error from step1 through step4
})
.done();

用generator处理的话就是

1
2
3
4
5
6
7
8
9
10
function* longRunningTask(){
try{
var value1=yield step();
var value2 = yield step2(value1);
var value3 = yield step3(value2);
var value4 = yield step4(value3);
}catch(e){
}
}

然后,使用一个函数,按次序自动执行所有步骤。

1
2
3
4
5
6
7
8
9
10
11
12
scheduler(longRunningTask());
function scheduler(task) {
setTimeout(function() {
var taskObj = task.next(task.value);
// 如果Generator函数未结束,就继续调用
if (!taskObj.done) {
task.value = taskObj.value
scheduler(task);
}
}, 0);
}

但是yield是同步运行,实际上都是yield语句都是返回Promise对象.

1
2
3
4
5
6
7
8
9
10
var Q=require("q")
function delay(milliseconds) {
var deferred = Q.defer();
setTimeout(deferred.resolve, milliseconds);
return deferred.promise;
}
function* f(){
yield delay(100);
};

部署iterator接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function* iterEntries(obj) {
let keys = Object.keys(obj);
for (let i=0; i < keys.length; i++) {
let key = keys[i];
yield [key, obj[key]];
}
}
let myObj = { foo: 3, bar: 7 };
for (let [key, value] of iterEntries(myObj)) {
console.log(key, value);
}
// foo 3
// bar 7