ES6小计(4)

13. Iterator和for…of

13.1 Iterator遍历器

遍历器创建一个指针对象,通过不断调用指针对象的next方法移动指针指向,不断对对象成员进行遍历。拥有遍历器接口的对象,均可以使用for...of进行遍历。数组、类数组、Set结构、Map结构自带遍历器接口,可直接使用for...of进行遍历。

调用遍历器接口的场合:
数组和Set的结构赋值、扩展运算符、yield*状态机、for...ofArray.from()Map()Set()Promise.all()Promise.race()

字符串为类数组解构,也具有遍历器解构。

13.2 for…of循环

for...of用于遍历所有数据结构的统一方法。只要一个数据结构有Symbol.iterator属性,就可以用for...of进行遍历。

var arr = ['a', 'b', 'c']
for (let a in arr) { console.log(a) } // 0 1 2
for (let a of arr) { console.log(a) } // a b c

for...in用于遍历键名,for...of用于遍历键值。如果要通过for...of遍历键名,可以借助数组实例的entries()keys()方法进行遍历。
for...of遍历数组时,只会遍历数字索引的属性,与for...in有点不一致。

SetMapfor...of遍历:

var engines = new Set(['a', 'b', 'c', 'c'])
for (var e of engines) {
    console.log(e) 
}
// a
// b
// c
var es6 = new Map()
es6.set('a', 1)
es6.set('b', 2)
es6.set('c', 3)
for (var [key, value] of es6) {
    console.log(key + ':' + value)
}
// a:1
// b:2
// c:3

遍历的顺序是按添加顺序进行的,Set遍历为一个值,Map遍历时一个键和值组成的数组。对于有的不含遍历接口的类数组{0: 'a', 1: 'b', length: 2},可以先用Array.from()将其转换为数组后遍历。
for...of不能遍历普通对象。但可以用Object.keys()将对象的键名生成数组后进行遍历。

13.3 遍历比较

for循环:最原始,书写较麻烦。
数组的forEach循环:arr.forEach(function(value) { console.log(value) }),缺点是无法跳出循环。
for...in循环:不仅遍历数字键名,还遍历原型链上的键,这往往不是我们想要的,且遍历有时顺序随机。
for...of循环:没有上述的缺点。

14. Generator函数

14.1 简介

Generator函数是一种新的JS异步编程解决方案。可以把它理解为一个状态机,在这个状态机内,有多个需要依次去检测其状态的异步函数。Generator函数会返回一个遍历器对象,故其也是一个遍历器对象生成函数。

Generator函数有两个特征:一是在function与函数名之间有一个*号;而是内部用yield定义不同的状态。

function *temp() {
    yield 'a';
    yield 'b';
    return 'c'
}

let hw = temp()
hw.next() // {value: 'a', done: false}
hw.next() // {value: 'b', done: false}
hw.next() // {done: true}

Generator函数的调用方法与普通函数一样,但因Generator函数返回的是一个遍历器对象,故函数内容并没有执行,需要用遍历器对象的next方法去遍历执行。每次调用next方法,内部指针就从函数的头部或上一次停下来的地方开始执行,执行到下一个yield(会执行完遇到的这个yield语句)或者return语句为止。

从上述变现看,Generator函数是一个可以暂停执行的函数,yield就是暂停语句。yield暂停语句不能用在普通函数中,会报错。若yield语句在表达式中用于计算时,需要在()内使用。用于赋值表达式的又边时,可以不加括号。

14.2 next 方法参数

yield语句本身没有返回值,但next()方法可以带一个参数,携带的这个参数会被当做上一条yeild语句的返回值。故可以通过next()方法向函数内部注入控制值,但又有函数的上下文环境。

function *foo(x) {
    var y = 2 * (yield (x + 1))
    var z = yield (y / 3)
    return x + y + z
}
var a = foo(5)
a.next() // {value: 6, done: false}
a.next() // {value: NaN, done: false} 注意yield无返回值,y = 2 * undefined == NaN
a.next() // {value: NaN, done: true}
var b = foo(5)
b.next() // {value: 6, done: false}
b.next(12) // {value: 8, done: false} // y = 2 * 12
b.next(13) // {value: 42, done: true} // x = 5, y = 24, z = 13

第一条next语句不能有参数,因参数代表上一条语句的返回值。当然,可以在Generator函数外再包一层已有一个自执行的next()方法的状态机函数就行了。

14.3 for...of循环

for...of可以自动遍历Generator函数,不需要使用next方法。当next方法返回对象的done属性为true时,for...of循环终止,并且不包含返回对象。

function *foo() {
    yield 1
    yield 2
    yield 3
    return 4
}
for (let v of foo()) {
    console.log(v)
}
// 1 2 3

14.4 Generator.prototype.throw()

Generator函数返回的遍历器对象都有一个throw方法,可以在函数体外抛出错误,但是错误的冒泡,会从函数体内开始。类似于错误注入到状态机内。注入的位置为yield关键字位置,在后续语句执行前。若此处上下文没有进行错误捕获,错误则会冒泡到函数外部。类似于Promisereject

var g = function *() {
    while (true) {
        try {
            yield;
        } catch (e) {
            if (e != 'a') throw 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方法,将返回{value: undefined, done: true}对象。

14.5 Generator.prototype.return()

Generator函数返回的遍历器对象都有一个return方法,可以返回给定的值,并终止遍历。

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}

return()不传值时,返回值valueundefined

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}

注意当returntry代码块内返回时,要等到finally结束后才会执行return返回值。finally内等同于try外部,但是try后会执行。

14.6 yield* 语句

如果在Generator函数内部调用另外一个Generator函数,默认情况下是没有效果的,因为Generator函数仅仅返回的是一个遍历器对象。但我们想要这个遍历器对象也生效时,就要用yield*方法调用内部Generator函数了,类似于对于这个遍历器对象做for...of遍历。其实yield*语法完全可以用for...of进行转换。

function *foo() { yield 'a'; yield 'b'; }
function *bar() { yield 'x'; foo(); yield 'y' }
function *set1() { yield 'x'; yield* foo(); yield 'y' }
function *set2() { yield 'x'; for (let v of bar()) { yield v }; yield 'y' }
for (let v of bar()) {
    console.log(v)
}
// x
// y
for (let v of set1()) {
    console.log(v)
}
// x
// a
// b
// y
for (let v of set2()) {
    console.log(v)
} 

上面例子最后set2输出同set1一致,不过set2中for…of循环需要自己做yield处理。并不是像yield 语句一样,将内部遍历器对象与外部遍历器对象合并了。准确的说,是`yield`将内部遍历器对象提取到外部来了。

任何数据结构,具有Iterator接口,就可以使用yield*进行遍历,与for...of表现一致。若内部Generator函数内有return语句进行返回时,可以向外部Generator函数进行数据返回。如用yield*进行处理内部状态机时,有两步:将内部状态机遍历器对象提取到外部,返回内部return的返回值。

function *g() {
    yield 'a'
    yield 'b'
    return 'result:'
}
function *g2(send) {
    let result = yield* send()
    console.log(result)
}
console.log([...g2(g)])
// result:
// [ 'a', 'b' ]

故可以很方便地取出多重数组内部的值。

14.7 作为对象属性的Generator函数

let obj = {
    gen: function *() {}
}
// 等同于
let obj = {
    * gen() {}
}

14.8 Generator函数内的this

因为Generator函数返回的时一个遍历器对象,故在没有对遍历器遍历时,Generator内部逻辑是没有执行的,故内部的this无效。但可以使用bind显式地对内部this进行绑定,并且对遍历器对象进行遍历后,this才会有正确的指向。

function *g() {
    this.a = 1
}
let obj = g()
obj.a // undefined

function *f() {
    yield this.x = 2
    yield this.y = 3
}
let obj = {}
let temp = f.bind(obj)()
temp.next() // {value: 2, done: false}
temp.next() // {value: 3, done: false}
temp.next() // {value: undefined, done: true}
obj // {x: 2, y: 3}

14.9 应用

Generator函数的暂停执行效果,意味着把异步操作写在yield语句后,就可以通过next方法进行异步调用,顺序执行。

TO BE CONTINUED!