javascript迭代器

53 阅读7分钟

可迭代协议

可迭代协议允许 JavaScript 对象定义或定制它们的迭代行为,例如,在一个 [for..of]结构中,哪些值可以被遍历到。一些内置类型同时是内置的可迭代对象,并且有默认的迭代行为,比如 [Array]或者 [Map],而其他内置类型则不是(比如 [Object])。要成为可迭代对象,该对象必须实现  @@iterator 方法,这意味着对象(或者它原型链上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 [Symbol.iterator] 访问该属性:

  • [Symbol.iterator]: 一个无参数的函数,其返回值为一个符合迭代器协议的对象。

迭代器协议

迭代器协议定义了产生一系列值(无论是有限个还是无限个)的标准方式,当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值。只有实现了一个拥有以下语义的 next()  方法,一个对象才能成为迭代器:

next():

无参数或者接受一个参数的函数,并返回符合 IteratorResult 接口的对象。如果在使用迭代器内置的语言特征(例如 for...of)时,得到一个非对象返回值(例如 false 或 undefined),将会抛出 TypeError"iterator.next() returned a non-object value")。

所有迭代器协议的方法(next()return() 和 throw())都应返回实现 IteratorResult 接口的对象。它必须有以下属性:

done[可选]

为布尔值

  • true:迭代器已将序列迭代完毕,这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。
  • false:迭代器能够生成序列中的下一个值(等价于没有指定 done 这个属性)

value[可选]

迭代器返回的任何 JavaScript 值。done 为 true 时可省略。

实际上,两者都不是严格要求的;如果返回没有任何属性的对象,则实际上等价于 { done: false, value: undefined }

如果一个迭代器返回一个 done: true 的结果,则对任何 next() 的后续调用也返回 done: true

return(value)[可选]

无参数或者接受一个参数的函数,并返回符合 IteratorResult 接口的对象,其 value 通常等价于传递的 value,并且 done 等于 true。调用这个方法表明迭代器的调用者不打算调用更多的 next(),并且可以进行清理工作。

throw(exception)[可选]

无参数或者接受一个参数的函数,并返回符合 IteratorResult 接口的对象,通常 done 等于 true。调用这个方法表明迭代器的调用者监测到错误的状况,并且 exception 通常是一个 [Error] 实例。

迭代器(Iterator)

JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了MapSet。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是MapMap的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。

Iterator就是这样一种机制。它是一种接口,具有特定结构的对象(其中包含一个 next 方法,用于返回集合中的下一个元素),为各种不同的数据结构提供统一的访问机制。它不是新的语法或新的内置对象,而一种协议( 迭代器协议),所有遵守这个协议的对象,即包含 next,调用 next 返回一个result{value,done}。都可以称之为迭代器,完成遍历操作。

Iterator 接口的目的,就是为所有数据结构,提供一种统一的访问机制,即for...of循环。当使用for...of循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口,调用数据结构的Symbol.iterator方法。。

一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”

迭代器工作原理

  1. 创建一个迭代器对象,通常通过调用集合对象的 Symbol.iterator 方法来获取迭代器对象。
  2. 调用迭代器对象的 next 方法,每次调用都会返回一个包含 valuedone 两个属性的对象。
    • value 表示集合中的一个元素
    • done 表示迭代是否已完成,如果为 true,则表示迭代结束;如果为 false,则表示还有更多元素可供遍历
  3. 重复调用 next 方法,直到迭代结束。

内置可迭代对象

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

数组

数组原生具有迭代器接口,部署在数据的Symbol.iterator上,所以调用这个属性就可以获取一个迭代器对象,然后使用迭代器的 next 方法逐个访问数组的元素。

const arr = [1, 2, 3, 4, 5];
const iterator = arr[Symbol.iterator]();
let result = iterator.next();
while (!result.done) {   //遍历器对象每次移动指针,都检查一下返回值的done属性,如果遍历还没结束,就移动遍历器对象的指针到下一步,不断循环
    console.log(result.value);    //1,2,3,4,5
    result = iterator.next();
}

Map

Map原生具有迭代器接口,可直接使用for...of遍历,此处通过调用 Map 对象的 entries() 方法获取一个迭代器对象,然后使用迭代器的 next 方法逐个访问 Map 的键值对。

const map = new Map([["a", 1], ["b", 2], ["c", 3]]);
const iterator = map.entries();

let result = iterator.next();
while (!result.done) {
    const [key, value] = result.value;
    console.log(key, value);        //a 1,b 2,c 3
    result = iterator.next();
}

Set

Set原生具有迭代器接口,可直接使用for...of遍历,此处通过调用 Set 对象的 values() 方法,获取一个迭代器对象,然后使用迭代器的 next 方法逐个访问 Set 的元素。

const set = new Set([1, 2, 3, 4, 5]);
const iterator = set.values();

let result = iterator.next();
while (!result.done) {
    console.log(result.value);    //1,2,3,4,5
    result = iterator.next();
}

对象

对于原生部署 Iterator 接口的数据结构,不用自己写遍历器生成函数,for...of循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的 Iterator 接口,都需要自己在Symbol.iterator属性上面部署,这样才会被for...of循环遍历。

对象(Object)之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作 Map 结构使用,ES5 没有 Map 结构,而 ES6 原生提供了。

//通过将字面量对象的key转为数组,然后调用数组的迭代器遍历对象:
const obj = {
    name: 'aye',
    age: 18,
    sex: '男'
}
const iterator = Object.keys(obj)[Symbol.iterator]();
let result = iterator.next();
while (!result.done) {
    let key = result.value;
    console.log(`${key}--${obj[key]}`);    //name--aye,age--18,sex--男
    result = iterator.next();
}

类数组对象:

const obj = {
    0: 'a',
    1: 'b',
    2: 'c',
    length: 3,
    [Symbol.iterator]: Array.prototype[Symbol.iterator]
}
for (let v of obj) { 
    console.log(v);     //a,b,c
}
//注意,普通对象部署数组的Symbol.iterator方法,并无效果。

通过生成器函数遍历普通对象:

const obj = {
    name: 'aye',
    age: 18,
}
function* entries(obj) {
    for (let key of Object.keys(obj)) {
        yield [key, obj[key]]
    }
}

for (let [key, value] of entries(obj)) {
    console.log(`${key}--${value}`)   //name--aye   age--18
}

类部署迭代器接口:

class Range{
    constructor(start, end) {
        this.start = start;
        this.end = end;
    }
    [Symbol.iterator]() {
        return this;
    }
    next() {
        const value = this.start;
        if (value < this.end) {
            this.start++;
            return {done: false, value};
        }
        return {done: true, value: undefined};
    }
}
function r(start, end) {
    return new Range(start, end);
}

for (const value of r(1, 5)) { 
    console.log(value);     //1,2,3,4
}

自定义迭代器

const myIterable = {
    data: [1, 2, 3, 4, 5],
    [Symbol.iterator]() {
        let index = 0;

        return {
            next: () => {
                console.log(index)  //0,1,2
                if (index < this.data.length) {
                    return { value: this.data[index++], done: false };
                } else {
                    return { value: undefined, done: true };
                }
            },
            return: () => {   //如果`for...of`循环提前退出(通常是因为出错,或者有break语句),就会调用return()方法
                console.log('Closing')
                return { value: undefined, done: true };
            }
        };
    },
};
for (const item of myIterable) {
    console.log(item);    //1,2,3
    if (item === 3) {
        break;      //Closing
    }
}

会默认调用迭代器接口的场景

  • 解构赋值
  • 扩展运算符
  • for...of
  • yield *
  • Promise.all()
  • Promise.race()
  • Map()
  • Set()
  • WeakMap()
  • WeakSet()