1

iterator迭代器

在ES6之前遍历数组的方法有以下四种:

// 第一种
for(var i = 0; i < array.length; i++){
    console.log(array[i])
}
// 第二种
array.forEach(function(item,index){
    console.log(item)
})
// 第三种
for(var index in array){
    console.log(array[index])
}
// 第四种
for(var value of array){
    console.log(value)
}

在上面的遍历方式中,第二种方式有一种小缺陷,就是不能使用break语句中断执行,也不能使用return语句返回到外层函数。它会一直遍历完数组的所有元素。第三种方式是一个更糟糕的方式,在遍历过程中,赋值给index的不是number类型,而是字符串类型,for-in循环除了遍历数组外,还会遍历自定义属性,甚至是遍历出原型链上的属性。并且for-in的遍历顺序不能得到保障。

第四种方法是ES6中新增的遍历数组的方法,它可以正确响应break、continuereturn语句,for-in语句除了能遍历数组外,还能遍历类数组对象,如DOM的NodeList对象,arguments对象,也能遍历字符串、Set对象、Map对象。

for-of循环语句能够遍历各种集合的。是因为这些对象都有一个迭代器的方法,迭代器(Iterator)是一种接口,为各种不同的数据结构提供统一的访问机制,任何数据结构只要部署了iterator接口,就可以完成遍历操作。比如下面这种情况:

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}
                }
            }
        }
    }
}
for(var value of obj){
    console.log(value)    // 输出   hello  world
}

迭代器也可以直接使用Array默认的iterator。

// 也可以直接使用Array的迭代器
let newObj = {
    0:'a',
    1:'b',
    2:'c',
    [Symbol.iterator]:Array.prototype[Symbol.iterator]
}
for(var value of newObj){
    console.log(value)   // 输出 a b c
}

for-of循环语句其实是调用遍历对象的[Symbol.iterator]方法,该方法返回一个iterator,里面有一个next方法,for循环会不断调用这个iterator.next方法来获取下一个值,直到返回值中的done属性为ture时结束循环。除了添加iterator外还可以使用yield实现循环:

let obj = {
    [Symbol.iterator]: function* (){
        for(var i = 0; i < 100; i++){
            yield i
        }
    }
}
for(var value of obj){
    console.log(value)
}

iterator的遍历过程

  1. 创建一个指针对象,指向当前数据结构的起始位置,也就是说,遍历器对象的本质就是一个指针对象。
  2. 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
  3. 不断调用指针对象的next方法,直到它指向数据结构的结束位置。

当使用for-of循环遍历某个数据结构时,该循环会自动去寻找iterator接口。只要一个对象含有iterator接口,那么该对象就可以被遍历。

使用iterator的场景

  • 解构赋值
let arr = [1,2,3,5]
let [first,...second] = arr
  • 扩展运算符
var str = 'hello'
[...str]    //  ['h','e','l','l','o']
  • yield*

yield*后面跟的是一个可遍历的结构,它就会调用该结构的遍历器接口。

let generator = function* (){
    yield* [2,3,4]
}
  • 其他场合

由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。比如for-of、Promise.all()、Promise.race()

生成器Generators

生成器函数与普通函数不同的点:普通函数使用function关键字声明,生成器函数使用function*声明,在生成器函数内部,有类似return语法的yeild关键字,与return不同的是,生成器函数可以yeild多次,在函数执行过程中,遇到yield表达式立即暂停,后续可恢复执行状态。

function* question(name){
    yield "你好" + name + '!'
    yield "希望你能喜欢"
    yield "下次再见"
    return '拜拜'
}
var iter = question('小明')
iter.next()    // {value: "你好小明!", done: false}
iter.next()   // {value: "希望你能喜欢", done: false}
iter.next()    // {value: "下次再见", done: false}
iter.next()    // {value: '拜拜', done: true}

generator函数在调用后,并不会立即执行,而是返回一个iterator对象,每次生成器执行到yield语句后,生成器函数的堆栈结构(本地变量、参数、临时值、生成器内部当前的执行位置)被移除堆栈。然而,生成器对象保留了对这个堆栈结构的引用,所以稍后调用.next()方法可以重新激活堆栈结构并且继续执行。不过生成器不是线程,它仍然处于JS单线程里。

如果运行到后面没有yield表达式,就会一直运行到函数结束,直到return语句为止,并将return表达式的值赋值给最后返回对象的value值,如果没有return语句,则返回对象的value值为undefined。
generator生成器是iterator的生成函数,执行generator函数,返回的就是iterator迭代器。

next方法的参数

function* gen(){
    for(let i = 0; true; i++){
        let reset = yield i
        if(reset){
            i = -1
        }
    }
}
let g = gen()
g.next()     // {value:0,done:false}
g.next(true)    // {value:0;done:false}

yield表达式本身没有返回值,或者说总是返回undefined,next方法可以带一个参数,该参数就会被作为上一个yeild表达式的值。generator从暂停状态到恢复运行,它的上下文状态是不变的,通过next方法传入参数,可以在generator函数开始运行之后,继续向函数内部注入值。

throw方法

function* gen(){
    try {
        yield '123'
    }catch(e){
        console.log('内部捕获',e)
    }
}
let g = gen()
g.next()
try {
    g.throw('a')
    g.throw('b')
}catch(e){
    console.log('外部捕获','b')
}
// 内部捕获 a
// 外部捕获 b

上述代码中,遍历器对象g连续抛出错误,第一个错误被generator函数体内的catch语句捕获,第二次抛出错误时,由于catch语句已经执行过了,不会捕获该错误,所以这个错误由函数体外的catch捕获。如果函数内部没有try-catch语句,那么throw方法抛出的错误将被外部的try-catch捕获。遍历器对象g调用throw方法后,抛出的异常被generator函数体捕获以后,会附带执行下一条yield语句。一旦generator执行过程抛出错误,且没有被内部捕获,generator函数就不会执行下去了。如果此后再调用next方法,将返回对象{value:undefined,done:true}

return方法

function* gen(){
    yield 1;
    yield 2;
    yiled 3;
}
var g = gen()
g.next()      // {value:1,done:false}
g.return('end')  // {value:'end',done:true}
g.next()     // {value:undefined,done:true}

如果generator函数内部有try-finally代码块,那么return方法会推迟到finally代码块执行完毕再执行。

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 }

next()、throw()、return()这三个方法都是让generator函数恢复执行,并且使用不同的语句替换yield表达式,next()是将yeild表达式替换成一个值,throw()是将yeild替换成一个throw语句,return()是将yield语句替换成一个return语句。

yield*

在generator函数内部再调用另一个generator函数,默认情况下是没有效果的,这个时候就需要用到yield*表达式,用来在一个generator函数内执行另一个generator函数。


熟悉的陌生人
330 声望17 粉丝

如果我有一本书...