JavaScript筑基(七):Iterator & Generator

230 阅读4分钟

Iterator & generator

Iterator

iterator 迭代器是使用户可以对某个数据结构进行遍历的对象,使用该接口无需关心对象的内部实现细节

这里的数据结构在JavaScript中包括数组StringSetMaparguments 对象、NodeList 集合。这些对象内置了迭代器。

在JavaScript中,迭代器是一个具体的对象,这个对象要符合迭代器协议(实现 next 函数)

next 函数

这是一个函数,返回一个应当拥有以下两个属性的对象

  • done:如果迭代器可以产生序列中的下一个值,则返回 false,如果已经迭代完毕,没有后续,那就是 true
  • value:迭代器返回的任何JavaScript值
const arr = ["a", "b", "c"];
const iterator = arr[Symbol.iterator]();  // js的迭代器存在于对象的 [Symbol.iterator] 属性上

console.log(iterator.next()); // { value: 'a', done: false }
console.log(iterator.next()); // { value: 'b', done: false }
console.log(iterator.next()); // { value: 'c', done: false }
console.log(iterator.next()); // { value: undefined, done: true }

实现 Symbol.iterator

想要实现上例中原生可迭代对象的迭代器还是比较简单的,只要记录 next 被调用的次数就可以,我们这里用一个闭包来实现

function myIterator() {
  let index = 0;
  return {
    next: () => {
      return index < this.length
        ? { value: this[index++], done: false }
        : { value: undefined, done: true };
    },
  };
}

// 测试用例
Array.prototype[Symbol.myIterator] = myIterator;
const iterator = ["a", "b", "c"][Symbol.myIterator]();
 
console.log(iterator.next()); // { value: 'a', done: false }
console.log(iterator.next()); // { value: 'b', done: false }
console.log(iterator.next()); // { value: 'c', done: false }
console.log(iterator.next()); // { value: undefined, done: true }

应用场景

上文提到了可迭代对象,实现了 [Symbol.iterator] 的对象就可以被称为可迭代对象。而这些可迭代对象可以使用一些语法糖,如 for...of...展开语法

对应的,非可迭代对象使用这些语法糖就会报错

const obj = { // 默认对象不是可迭代的
  values: ["a", "b", "c"]
}

for (const value of obj) {
  console.log(value); // TypeError: obj is not iterable
}
console.log(...obj)   // TypeError: Found non-callable @@iterator

我们可以添上自定义内置的迭代器

const obj = {
  values: ["a", "b", "c"],
  [Symbol.iterator]: function () {  // 自定义可迭代对象
    let index = 0;
    return {
      next: () => {
        return index < this.values.length
          ? { value: this.values[index++], done: false }
          : { value: undefined, done: true };
      },
    };
  },
};

for (const value of obj) {
  console.log(value); // a b c
}

console.log(...obj);  // a b c

要注意的是,语法糖的停止条件都是 done 的值,如果 donetrue 就会停止,不然会一直搜索下去: 在 for...of 中会陷入死循环(一直输出 undefined),...语法 则是会在当前语句卡住不输出任何值。

其它常用场景

  1. 解构语法 const [a, b] = iterableObj
  2. 创建其他对象 new Set(iterableObj)
  3. 一些API Array.from(iterableObj)Promise.all(iterableObj) ...

自定义相关类

既然已经知道了怎么写内置迭代器和迭代器的应用场景,那么实现一个跟 Array 一样可迭代的类已经很轻松了

class Person {
  constructor(...args) {
    this.names = args;
  }

  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        return index < this.names.length
          ? { value: this.names[index++], done: false }
          : { value: undefined, done: true };
      },
    };
  }
}

// 测试用例
let group = new Person("neo", "john", "amy");
console.log(...group);  // neo john amy

Generator

generator 生成器是一种特殊的迭代器,用于控制函数什么时候暂停与执行。在开发中并不多见,但是它与 Promise 构成的语法糖 async / await 则是特别高频的被使用。

虽然说生成器是一个特殊的迭代器,但是它并不是直接通过类似于 [Symbol.iterator] 这种函数返回的,而是通过生成器函数返回的

生成器函数

关于生成器函数只要关注这四点

  • 通过 function * 声明
  • 通过 yield 关键字控制函数执行流程
  • 返回值是个 generator
  • 返回的 generator 通过 next() 来操作下一步的流程

举个例子

function* foo() {
  console.log("start");
  yield;

  console.log("1");
  yield;

  console.log("2");
  yield;

  console.log("finish");
}

// 测试用例
const generator = foo();
generator.next(); // start
generator.next(); // 1
generator.next(); // 2
generator.next(); // finish

yield

yield 是个关键字,可以返回一个值,也可以接收

基本使用

先来看看返回值

function* foo() {
  // 可以像 return 一样返回值
  yield 1;
  yield 2;
}

const generator = foo();
const res1 = generator.next();  // { value: 1, done: false }
const res2 = generator.next();  // { value: 2, done: false }
const res3 = generator.next();  // { value: undefined, done: true }

console.log(res1.value, res2.value, res3.value);  // 1 2 undefined

再来看看接收值

function* foo() {
  console.log("start");

  const value1 = yield;
  console.log(value1);  // bbb

  console.log(yield);  // ccc

  console.log("finish");
}

const generator = foo();
generator.next("aaa");
generator.next("bbb");
generator.next("ccc");

当然,也可以组合使用

function* foo() {
  console.log("start");

  const value1 = yield 1;
  console.log(value1);  // bbb

  console.log(yield 2); // ccc

  console.log("finish");
}

const generator = foo();
const res1 = generator.next("aaa").value;
const res2 = generator.next("bbb").value;
const res3 = generator.next("ccc").value;

console.log(res1, res2, res3);  // 1 2 undefined

暂停执行时机

看完几个例子,第一个疑惑可能是,我在 next第一个传入的不是 aaa 吗,怎么没有被打印出来?

因为第一个 next 只是运行了第一个 yield 前面的代码片段,也没有 yield 可以接收第一个传入的参数。

那么随之而来第二个问题,第一个代码片段,是在哪里停止的,是含有 yield当前一行还是 yield上一行呢?

这里可以用一个例子来试试看

function* foo() {
  console.log("start");

  console.log(yield 1);

  console.log("finish");
}

const generator = foo();

const res1 = generator.next("aaa").value; 

console.log(res1);

// generator.next("bbb");
/* output

start
1

*/ 

这里的 res1 是有值的,而且还是 yield 返回的值,所以可以初步确定代码是执行到有 yield 的这一行。但是此时我们 console.log(yield 1) 并没有执行(甚至没有输出 undefined),那么我们可以猜想,yield 这里的语句可以被拆成两步

  1. 返回一个值
  2. 接收一个值并执行当前行语句

用代码表示console.log(yield 1)的两步如下

return 1 // 步骤一(与实际的 return 不同,在generator中使用return会将生成器的状态done设为true,终止生成器的活动)

console.log(yield)  // 步骤二(这里的yield代指通过启动第二个代码片段的next传入的参数)

我们启动最开始的例子里面的下一个next来验证一下

function* foo() {
  console.log("start");

  console.log(yield 1);

  console.log("finish");
}

const generator = foo();

const res1 = generator.next("aaa").value;

console.log(res1);

generator.next("bbb");
/* output

start
1
bbb
finish

*/ 

输出是符合预期的,至此证明了至少在实现的层面,可以将含有yield这一行的代码理解为两个步骤的结合

返回一个可迭代对象

通过 yield 返回一个可迭代对象,可以分段触发迭代器。是下面代码的语法糖

function* foo(arr) {
  for (const item of arr) {
    yield item;
  }
}

const generator = foo([1, 2, 3]);

const res = [];
for (let i = 0; i < 4; i++) {
  res.push(generator.next().value);
}
console.log(res); // [ 1, 2, 3, undefined ]

返回可迭代对象写法 yield*

function* foo(arr) {
  yield* arr; // yield 后面记得带 *
}

const generator = foo([1, 2, 3]);

const res = [];
for (let i = 0; i < 4; i++) {
  res.push(generator.next().value);
}
console.log(res); // [ 1, 2, 3, undefined ]

小结

最后,其实我们可以用生成器来简单实现自定义迭代器的功能

class Person {
  constructor(...args) {
    this.names = args;
  }

  [Symbol.iterator] = function* foo() {
    yield* this.names;
  };
}


let group = new Person("neo", "john", "amy");
console.log(...group); // neo john amy

后续

...正在施工中