js中的迭代器

99 阅读5分钟

迭代器

最简单也是最常见的迭代莫过于计数循环

for (let i = 0; i < 10; i++) {
  console.log(i);
}

循环是迭代的基础

ES6之后,js 也支持了迭代器模式。

迭代器模式

在迭代器模式下,把有些结构称之为可迭代对象(iterable),可迭代对象实现了Iterable接口。这个接口通过迭代器(iterator)消费。

可迭代对象很抽象,可以理解为数组或者集合这样的集合类型的对象:

  • 所包含元素有限
  • 具有无歧义的遍历顺序

需要注意的是,可迭代对象不一定是集合对象,也可以是仅仅具有类似数组行为的其他数据结构,就例如,开头提到的计数循环,它本身就是在执行迭代。截至到这里,可以认为计数循环和数组都有可迭代对象的行为

可以实现Iterator接口的结构“消费”能够实现Iterable接口的数据结构

迭代器iterator是按需创建的一次性对象,每个迭代器都会关联一个可迭代对 象

可迭代协议

实现Iterable接口(可迭代协议),需要具备两种能力:

  1. 支持迭代的自我识别能力
  2. 创建实现iterator接口的对象的能力

ECMAScript中,这意味着需要暴露一个属性作为默认迭代器,这个属性以Symbol.iterator作为,这个属性必须引用一个迭代器工厂函数,调用这个函数,必须返回一个新迭代器

很多内置类型都实现了Iterable接口:

  • 字符串
  • 数组
  • 映射
  • 集合
  • arguments 对象
  • NodeList 等 DOM 集合类型

检查是否存在默认迭代器属性可以通过Symbol.iterator来判断

let obj = {};
let num = 1;

// 显然对象和数字类型的数据没有实现迭代工厂函数
console.log(obj[Symbol.iterator]); //undefined
console.log(num[Symbol.iterator]); //undefined

let str = "string";
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let map = new Map().set([
  ["a", 1],
  ["b", 2],
]);
let set = new Set().add([1, 2, 3, 4, 1, 2, 3, 4]);

// 字符串、数组、map、set这些都实现了迭代器工厂函数
console.log(str[Symbol.iterator]); //[Function: [Symbol.iterator]]
console.log(arr[Symbol.iterator]); //[Function: values]
console.log(map[Symbol.iterator]); //[Function: entries]
console.log(set[Symbol.iterator]); //[Function: values]

// 调用迭代器工厂函数会产生一个新的迭代器
console.log(str[Symbol.iterator]());
console.log(arr[Symbol.iterator]());
console.log(map[Symbol.iterator]());
console.log(set[Symbol.iterator]());
// Object [String Iterator] {}
// Object [Array Iterator] {}
// [Map Entries] { [ [ [ 'a', 1 ], [ 'b', 2 ] ], undefined ] }
// [Set Iterator] { [
//     1, 2, 3, 4,
//     1, 2, 3, 4
//   ] }

实际上在写代码的过程中,并不需要显示的调用这个迭代器工厂函数, 已经实现了可迭代协议的所有类型,都会自动的兼容接收可迭代对象的任何语言特性。

接收可迭代对象的原生语言特性包括以下几种:

  • for-of 循环
  • 数组解构
  • 扩展操作符
  • Array.from()
  • 创建集合
  • 创建映射
  • Promise.all()接收由契约组成的可迭代对象
  • Promise.race()接收由契约组成的可迭代对象
  • yield*操作符,在生成器中使用

以上的原生语言结构会在后台调用提供的可迭代对象的这个迭代工厂函数,从而创建一个迭代器

迭代器协议

迭代器是一种一次性使用的对象,用于迭代与之关联的可迭代对象;

迭代器 API使用next()方法可以在可迭代对象中遍历数据,每次成功调用next()都会返回一个IteratorResult对象,这个对象有两个属性

  1. done:是一个布尔值,表示当前可迭代对象的数据是否迭代穷尽
  2. value:表示可迭代对象的下一个值,如果耗尽了那么返回undefined
  • 如果不调用next(),无法知道迭代器的当前位置

示例:

let arr = ["join", "Tom", "Bob"];

let iterator = arr[Symbol.iterator](); //迭代器
console.log(iterator.next()); //{ value: 'join', done: false }
console.log(iterator.next()); //{ value: 'Tom', done: false }
console.log(iterator.next()); //{ value: 'Bob', done: false }
console.log(iterator.next()); //{ value: undefined, done: true }

每个迭代器都表示对可迭代对象的一次有序的遍历,不同迭代器创建的实例是独立存在的(对一个对象创建两个迭代器对象,迭代是各自独立的)

  • 迭代器与可迭代对象之间不是通过某个时刻的可迭代对象进行快照绑定的,而是通过使用游标来记录当前遍历的可迭代对象的历程,也就是说在这个过程中,如果可迭代对象发生了变化,那么迭代器也会反映出相应的变化
let arr = ["join", "Tom", "Bob"];

let iterator = arr[Symbol.iterator](); //迭代器
console.log(iterator.next()); //{ value: 'join', done: false }
console.log(iterator.next()); //{ value: 'Tom', done: false }
console.log(iterator.next()); //{ value: 'Bob', done: false }

arr.splice(3, 0, "LAST");

console.log(iterator.next()); //{ value: 'LAST', done: false }

自定义迭代器

任何实现了Iterator接口的对象都可以当作迭代器使用

但是下面的例子产生的对象只能被迭代一次(因为这里我们的 number值是放在构造函数里的,那么创建实例之后,再一次迭代改变的还是实例属性上的值),所以要创建一个能多次迭代的实例主要就是这个number我们需要每次创建构造器都创建新的number

class CreateIteraor {
  constructor(limit) {
    this.limit = limit;
    this.number = 1;
  }

  next() {
    if (this.number <= this.limit) {
      return {
        done: false,
        value: this.number++,
      };
    } else {
      return {
        done: true,
        value: undefined,
      };
    }
  }
  [Symbol.iterator]() {
    return this;
  }
}

let myIterator = new CreateIteraor(3);
console.log("--------");
for (const item of myIterator) {
  console.log(item);
}
console.log(myIterator.next());
for (const item of myIterator) {
  console.log(item);
}
console.log("--------");
//--------
//1
//2
//3
//{ done: true, value: undefined }
//--------

让一个可迭代对象能够创建多个迭代器,必须每次创建一个迭代器就对应一个新的计数器。为此可以把计数器变量放到闭包里,然后通过闭包返回迭代器

class CreateIteraor {
  constructor(limit) {
    this.limit = limit;
  }
  [Symbol.iterator]() {
    let number = 1,
      limit = this.limit;
    return {
      next() {
        if (number <= limit) {
          return {
            done: false,
            value: number++, //不是this.number
          };
        } else {
          return {
            done: true,
            value: undefined,
          };
        }
      },
    };
  }
}

let myIterator = new CreateIteraor(3);
console.log("--------");
for (const item of myIterator) {
  console.log(item);
}
for (const item of myIterator) {
  console.log(item);
}
console.log("--------");
//--------
//1
//2
//3
//1
//2
//3
//--------

记住每个迭代器也实现了Iterable接口,也就是说每个迭代器也是一个可迭代对象

let a = ["john", "fred", "Tom"];

let iter = a[Symbol.iterator](); //迭代器
for (const item of a) {
  console.log(item);
}
//john
//fred
//Tom
for (const item of iter) {
  console.log(item);
}
//john
//fred
//Tom

提前终止迭代器

可选的return()方法用于指定在迭代器提前关闭时执行的逻辑,

执行迭代的结构在不想迭代器把他的可迭代对象耗尽的情况下就可以关闭迭代器,可能的情况包括:

  • for-of循环通过breakcontinuereturnthrow提前退出迭代
  • 解构操作未能完全消费可迭代对象的全部值

return()方法必须返回一个有效的IteratorResult对象,简单情况下这个对象可以只包含done : true,这个返回值会用在生成器的上下文中

class Iterator {
  constructor(limit) {
    this.limit = limit;
  }
  [Symbol.iterator]() {
    let number = 1;
    let limit = this.limit;

    return {
      next() {
        if (number <= limit) {
          return {
            done: false,
            value: number++,
          };
        } else {
          return {
            done: true,
            value: undefined,
          };
        }
      },
      return() {
        return {
          done: true,
        };
      },
    };
  }
}

let a = new Iterator(4);
for (const item of a) {
  if (item > 3) {
    break; //throw等都可以关闭迭代器
  }
  console.log(item);
}
//1,2,3

// 解构未完全消费所有值,也会导致提前退出
let [b, c] = a;
console.log(b, c); //1,2

如果迭代器没有关闭,则可以继续从上次离开的地方继续迭代,但是数组的迭代器就不可以关闭

let arr = [1, 2, 3, 4, 5];
let arrIterator = arr[Symbol.iterator]();

for (let item of arrIterator) {
  if (item > 3) {
    break;
  }
  console.log(item);
}
console.log("--------------");
for (let item of arrIterator) {
  console.log(item);
}
//1
//2
//3
//--------------
//5

因为return()方法是可选的,并不是所有的迭代器都是可以关闭的(数组的就不可以被关闭)。要知道一个迭代器是否可以关闭,可以测试这个迭代器实例的 return 属性是不是函数对象,但是如果你给一个不可关闭的迭代器添加上return属性,他会调用 return 方法,但是并不能改变他本身不可以关闭的状态