likes
comments
collection
share

JS - 迭代器的实现以及提前退出迭代器

作者站长头像
站长
· 阅读数 14

ES6 迭代器和扩展操作符可以让集合类型之间相互操作、复制和修改变得异常方便

迭代的意思: 按照顺序多次执行一段程序,终会有明确的终止条件

思维导图

JS - 迭代器的实现以及提前退出迭代器

4 种原生类型实现了迭代器

实现了迭代器,都可以使用 for-of 循环,也就以为着实现了迭代器的都可以兼容扩展操作符

const iterableContainer = [
  ["1str", "2str", "3str"],
  (typeArr = Int16Array.of(3, 4)),
  new Map([
    ["mk1", "mk-val"],
    ["mk2", "mk2-val"],
  ]),
  new Set([9, 10]),
];

for (const iterableSet of iterableContainer) {
  for (const element of iterableSet) {
    log(element);
  }
}

/**
依此打印:
1str
2str
3str

3
4

[ 'mk1', 'mk-val' ]
[ 'mk2', 'mk2-val' ]

9
10
 */

const arr = [1, 2, 3];

// 扩展运算符是一个浅复制
const shdowArr = [...arr];

log(arr); // log:  [1, 2, 3, 4]
log(shdowArr); // log: [1, 2, 3, 4]
log(arr === shdowArr); // log: false

// 构建数组的部分元素
const newArr = [22, 33, ...arr, 44, 55];

对于期待可迭代对象的构造函数,只需要传入一个可迭代对象就可以实现复制

// 期望可迭代对象的构造函数有 Map(iterator), Set(iterator)
const map1 = new Map([
  ["k1", "v1"],
  ["k2", "v2"],
]);

const cloneMap = new Map(map1);
log(map1); // log: Map(2) { 'k1' => 'v1', 'k2' => 'v2' }
log(cloneMap); // log: Map(2) { 'k1' => 'v1', 'k2' => 'v2' }

浅复制意味着只会复制对象的引用

const arr1 = [{}];
// arr2 只会复制对象的引用
const arr2 = [...arr1];

arr1[0].name = "zhang3";

log(arr2[0]); // log: { name: 'zhang3' }; 看,这就是浅复制的弊端

// 实现了迭代器的四种原生集合类型一般都有多种构建方法,也可以与扩展操作符一起使用,方便实现互操作
const arr = [1, 2, 3];
const i16Arr1 = Int16Array.of(...arr);
const arrFrom = Array.from(arr);

log(i16Arr1); // log: Int16Array(3) [ 1, 2, 3 ]
log(arrFrom); // log: [1,2,3]

// 把数组复制到 集合中
const set = new Set(arr);
log(set); // log: Set(3) { 1, 2, 3 }
// 把集合复制回数组中
const arrOfSet = [...set];
log(arrOfSet); // log: [1,2,3]

数组是 js 中有序集合最典型的例子

const arr = ["v1", "v2", "v3"];
for (let i = 0; i < arr.length; i++) {
  log(arr[i]); // 依此换行打印 v1,v2,v3
}

迭代器模式

迭代器模式是一种方案,即某些结构实现了正式的 Iterable 接口,并可通过 迭代器 Iterator 消费,通常这些结构被称作“可迭代对象”

任何实现了 Iterable 接口的数据结构都可以被实现了 Iterator 接口的结构消费(consume),迭代器是按需创建的一次性对象,每个迭代器会关联一个可迭代对象,迭代器会暴露迭代其关联可迭代对象的 API,迭代器无需了解与其关联的可迭代对象的结构,只需要知道如何取得连续的值(概念上的分离是 Iterable 和 Iterator 的强大之处)

可迭代对象

可迭代对象是抽象说法,具有无歧义的遍历顺序,可迭代对象不一定是集合对象,临时性可迭代对象可以实现为生成器。

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

    • 支持迭代的自我识别能力
    • 创建实现 Iterator 接口的对象的能力。这个属性在 JS 中必须使用 Symbol.iterator(默认迭代器) 作为键,这个键必须引用一个迭代工厂函数,这个工厂函数必须返回一个新的迭代器(包含 next 为其迭代方法)

实现了 Iterable 接口(可迭代协议)的内置类型

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

检查是否实现了 Iterable 接口可以使用默认迭代器属性 Symbol.Iterator

const num = 1,
  str = "str",
  obj = {},
  arr = [1, 2],
  typeArr = new Int16Array(3),
  map = new Map([["k1", "v1"]]),
  set = new Set([1, 2, 3]);

// 如果返回了迭代器工厂函数则说明实现了 Iterable 接口

// number 和 Object 类型没有实现 Iterable 接口
log(num[Symbol.iterator]); // log: undefined
log(obj[Symbol.iterator]); // log: undefined

// 如下都返回了 迭代器工厂函数,说明实现了 Iterable 接口
log(str[Symbol.iterator]); // log: [Function: [Symbol.iterator]]
log(arr[Symbol.iterator]); // log: [Function: values]
log(typeArr[Symbol.iterator]); // log: [Function: values]
log(set[Symbol.iterator]); // log: [Function: values]
log(map[Symbol.iterator]); // log: [Function: entries]

不需要显式调用迭代工厂函数来生成迭代器

实现可迭代协议(Iterable 接口)的所有类型都会自动兼容可迭代对象的人和语言特性,接受可迭代对象的原生语言特性包括:

  • for .. of 循环
  • 数组解构
  • 扩展操作符
  • Array.from(iterable)
  • 创建集合 new Set(iterable)
  • 创建映射 new Map
  • Promise.all(iterable) 接受由 Promise 组成的可迭代对象
  • Promise.race(iterable) 接受由 Promise 组成的可迭代对象
  • 生成器中 yield 操作符
  • 如果对象原型链上实现了 Iterable 接口,那么这个对象也就实现了这个接口

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

const arr = ["v1", "v2", "v3"];

// for of \ 数组解构 \ 扩展操作符 \ Array.from(iterator) \ new Set(iterator) 循环会自动后台调用提供的可迭代工厂函数,然后创建迭代器
for (const v of arr) {
  log(v); // 依此换行打印: v1,v2,v3
}

// 数组解构 会自动后台调用提供的可迭代工厂函数,然后创建迭代器
const [val0, val1, val3] = arr; // 解构名字可以随意取,会按照顺序进行赋于相应值的
log(val0, val1, val3); // log: v1 v2 v3

// 扩展操作符 会自动后台调用提供的可迭代工厂函数,然后创建迭代器
log(...arr); // log: v1 v2 v3

// Array.from(iterable) 会自动后台调用提供的可迭代工厂函数创建迭代器
log(Array.from(arr)); // log: [ 'v1', 'v2', 'v3' ]

// Set 会自动后台调用提供的可迭代工厂函数创建迭代器
log(new Set(arr)); // log: Set(3) { 'v1', 'v2', 'v3' }

log(new Map(arr.map((v, i) => [v, i]))); // log: Map(3) { 'v1' => 0, 'v2' => 1, 'v3' => 2 }

// Array 是实现了 Iterable 接口的,因此它的子类 (继承了它) 的也就实现了 Iterable 接口
class CustomArr extends Array {}

const iArr = new CustomArr(1, 2, 3);
for (const iv of iArr) {
  log(iv); // 依此换行打印 1 2 3
}

迭代器协议

迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象,迭代器 API 使用 nex() 方法在可迭代对象中遍历数据,每次成功调用 next(),都会返回一个 IteratorResult 对象,其中包含迭代器返回的下一个值,不调用 next() ,则无法知道迭代器的当前位置

next() 返回的 IteratorResult 包含两个属性,next() 方法会按顺序进行迭代

    • done (boolean 类型)表示是否可以再次调用 next() 获取下一个值
    • value 当前的值,如果 done 为 true,则 value 为 undefined

迭代器可以指通用的迭代、也可以指接口(如实现了 Iterable)接口、也可以指正式的迭代类型

// 实现了 Iterable 接口(即可迭代协议)都是可迭代对象
const arr = ["v1", "v2"]; // 数组是 可迭代对象

// 获取可迭代对象的迭代器, Symbol.iterator 是一个 迭代器工厂函数,调用他会返回迭代器
const iter = arr[Symbol.iterator]();
// 执行迭代
log(iter.next()); // log: { value: 'v1', done: false }, done 为 false 说明还可以迭代获取到值
log(iter.next()); // log: { value: 'v2', done: false }
log(iter.next()); // log: { value: undefined, done: true }, done 为 true,说明迭代结束,后续结果都是 done 为false ,value 为 undefined
log(iter.next()); // log: { value: undefined, done: true }

迭代器不知道怎么从可迭代对象中取得下一个值,也不知道可迭代对象有多大,只要迭代器达到 done 为 true,后续调用 next() 就会返回同样的结果值了

每个迭代器都是独立的

每个迭代器都表示可迭代对象的一次性有序遍历。不同迭代器的实例之间没有关系,因此独立遍历可迭代对象没有问题

⚠️: 迭代器维护着一个只想可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可迭代对象

// 实现了 Iterable 接口(即可迭代协议)都是可迭代对象
const arr = ["v1", "v2"]; // 数组是 可迭代对象

// 获取可迭代对象的迭代器, Symbol.iterator 是一个 迭代器工厂函数,调用他会返回迭代器
const iter = arr[Symbol.iterator]();

// 每个每个迭代器都是独立的
const otherIter = arr[Symbol.iterator]();
// 执行迭代
log(iter.next()); // log: { value: 'v1', done: false }, done 为 false 说明还可以迭代获取到值
log(otherIter.next()); // log: { value: 'v1', done: false }
log(iter.next()); // log: { value: 'v2', done: false }
log(otherIter.next()); // log: { value: 'v2', done: false }

迭代器并不与可迭代对象某个时刻的快照绑定,而仅仅是使用游标来记录遍历可迭代对象,如果可迭代对象在迭代期间被修改了,那么迭代器也会反应相应的变化

const arr = ["v1", "v2"];
// 获取迭代器
const iter = arr[Symbol.iterator]();

log(iter.next()); // log: { value: 'v1', done: false }
// look! 可迭代对象在迭代期间被改了,迭代器也会相应的变化
arr.splice(1, 0, "value"); // 在 索引为 1 的位置插入一个值 value, arr 变为 ["v1", "value", "v2"];
log(iter.next()); // log: { value: 'value', done: false }
自定义迭代器

与 Iterable 接口相似,任何实现了 Iterator 接口的对象都可以作为迭代器使用

只能迭代一次的迭代对象
/**
 * 自定义迭代器
 * BadCounter 虽然实现了 Iterator 接口,但是每个实例只能被迭代一次
 */
class BadCounter {
  // BadCounter 的实例应该迭代 limit 次
  constructor(limit) {
    this.count = 1;
    this.limit = limit;
  }

  next() {
    const isDone = this.count > this.limit;
    return {
      done: isDone,
      value: isDone ? undefined : this.count++,
    };
  }

	// 在进行迭代的时候会 执行 this.next 的
  [Symbol.iterator]() {
    return this;
  }
}

const counter = new BadCounter(3);
for (const i of counter) {
  log(i); // log: 依此换行print 1、2、3
}

// 这里不能再进行迭代,因为 done 和 实例的 this.count 有关
for (const i of counter) {
  log(i);
}
可迭代多次的迭代对象
class Counter {
  constructor(limit) {
    if (!limit) {
      throw TypeError("limit 必须传递");
    }
    this.limit = limit;
  }

  [Symbol.iterator]() {
    // 为了能够让可迭代对象创建多个迭代器,必须每创建一个迭代器就对应一个新计数器 count
    let count = 1;
    // 这里使用闭包的目的是因为在 next 内部 this 不再指向当前实例
    const instanceLimit = this.limit;

    return {
      next() {
        // Symbol.iterator 的函数需要返回一个含有 next 的迭代器
        const isDone = count > instanceLimit;
        // next 需要返回一个 IteratorResult 对象
        return {
          done: isDone,
          value: isDone ? undefined : count++,
        };
      },
    };
  }
}

const counter = new Counter(3);

for (const c of counter) {
  log(c); // 依此换行 log 1、2、3
}

// 支持多次迭代
for (const c of counter) {
  log(c); // 依此换行 log 1、2、3
}
提前终止迭代器

return () 方法可用于指定在迭代器提前关闭时执行的逻辑。通常“关闭”迭代器的情况可能包括:

说通常的原因是因为并不一定就完全关闭了迭代器

  • for-of 循环通过 break、continue、return 或 throw 提前退出
  • 解构操作并为消费所有值

即如上情况会自动调用给你 return() 方法

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

// 提前终止迭代器
class Counter {
  constructor(limit) {
    this.limit = limit;
  }

  [Symbol.iterator]() {
    let count = 1,
      limit = this.limit;

    return {
      next() {
        const isDone = count > limit;
        return { done: isDone, value: isDone ? undefined : count++ };
      },
      // 终止迭代器的时候会调用 return 方法
      return() {
        log("Exiting early");
        return { done: true };
      },
    };
  }
}

const counter = new Counter(5);

for (const i of counter) {
  if (i > 2) {
    break; // break 的时候会调用 iteratorResult 的 return 方法,终止迭代
  }

  log(i); // 只会打印 1、2
}

log("---------split-------");
try {
  for (const i of counter) {
    if (i > 2) {
      throw new Error("mock error"); // throw 错误 的时候会调用 iteratorResult 的 return 方法,终止迭代
    }

    log(i); // 只会打印 1、2
  }
} catch (error) {}

log("---------split-------");

// 解构操作并为消费所有值也会 也会提前 待用 return 方法,终止迭代器
const [v1, v2] = counter; // 因为这里 counter 实现了 Iterator ,所以可以解构
log(v1, v2); // 1 2
return 方法

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

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

for (const i of iterArr) {
  log(i); // 这里依此打印 1、2、3
  if (i > 2) {
    break;
  }
}

log("---------split-------");

for (const i of iterArr) {
  log(i); // 这里从上一次 退出的地方开始迭代,打印 4、5 值
}

return() 方法是可选的,并非所有的可迭代器都是可关闭的。

Q: 怎样迭代器是否可以关闭?

A: 检测这个迭代器的 return 属性是不是一个函数;不过,仅仅给一个不可关闭的迭代器增加这个方法并不能让它变成可关闭的。因为可能调用 return() 不会强制迭代器进入关闭状态,但是 return() 方法还是可以别调用

const arr = [1, 2, 3, 4];
// 获取数组的迭代器
const iter = arr[Symbol.iterator]();

log(iter.return); // log: undefined, 说明数组的迭代器不可以关闭

// 给数组的 迭代器添加 return 方法,尝试关闭迭代器看看
iter.return = function () {
  log("Exiting");
  return { done: true };
};

for (const i of iter) {
  log(i); // 换行依此 打印出 1、2、3
  if (i > 2) {
    break; // 提前退出
  }
}

// 迭代器仍然没有关闭,因此下一个迭代会从上一次退出的地方继续迭代,说明 return 方法不管用
for (const i of iter) {
  log(i); // 继续从上一次退出的地方打印 4
}