JS `Iterator` 与 `Generator` 模式:自定义可迭代对象与惰性求值

咳咳,各位观众老爷们,晚上好!我是你们今晚的JS讲师,咱们今晚聊点有意思的,关于JS里“Iterator”(迭代器)和“Generator”(生成器)这哥俩,以及怎么用它们打造我们自己的可迭代对象,外加体验一下“惰性求值”的快感。

一、啥是迭代器?为啥我们需要它?

想象一下,你有一堆东西,比如一个数组[1, 2, 3, 4, 5]。你想一个一个地把它们拿出来,做点处理,比如打印出来,或者加个1啥的。 你肯定不想每次都手动去 array[0]array[1],这样写代码吧? 太low了!

这时候,迭代器就派上用场了。它可以像一个“指针”一样,指向数组中的某个元素,并且提供一种统一的方式,让你能一个一个地访问到数组里的所有元素,而不用关心数组内部是怎么实现的。

简单来说,迭代器就是一个对象,它定义了一个序列,并在终止时返回一个值。更具体地说,它是一个具有 next() 方法的对象,该方法返回一个包含 valuedone 两个属性的对象。

  • value:序列中的下一个值。
  • done:一个布尔值,指示迭代器是否已完成。如果为 true,则迭代器已到达序列的末尾,并且 value 可以被忽略。

代码示例:

const myArray = [1, 2, 3];

const iterator = myArray[Symbol.iterator](); // 获取数组的迭代器

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

看到了没? myArray[Symbol.iterator]() 这玩意儿返回的就是一个迭代器。 Symbol.iterator 是一个特殊的 symbol,它定义了对象的默认迭代器。 然后,我们调用 iterator.next() 就可以一个一个地拿到数组里的元素了。 当 donetrue 的时候,就说明迭代器已经到头了,没东西可拿了。

为啥我们需要迭代器?

  1. 统一的访问方式: 不管是数组、字符串、Set、Map,还是你自己定义的奇奇怪怪的数据结构,只要实现了迭代器,你就可以用统一的方式(比如 for...of 循环)来访问它们。
  2. 代码更简洁: 告别 for (let i = 0; i < array.length; i++) 这种老掉牙的写法吧!用 for...of 循环,代码更简洁易懂。
  3. 支持惰性求值: 这个后面会详细讲,简单来说,就是只有在你需要用到某个值的时候,才去计算它,可以节省资源。

二、 for...of 循环:迭代器的最佳搭档

for...of 循环是专门用来遍历可迭代对象的。 它会自动调用对象的迭代器,并且每次循环都会拿到迭代器返回的 value 值。

代码示例:

const myArray = [1, 2, 3];

for (const element of myArray) {
  console.log(element); // 依次打印 1, 2, 3
}

是不是比 for 循环简洁多了? 而且,for...of 循环不仅可以遍历数组,还可以遍历字符串、Set、Map 等等,只要它们实现了迭代器。

三、如何创建自定义的可迭代对象?

现在,我们来学习如何创建自己的可迭代对象。 这可是个很有用的技能,以后你想定义任何奇葩的数据结构,都可以让它支持迭代。

要让一个对象变成可迭代的,你需要做两件事:

  1. 实现 Symbol.iterator 方法: 这个方法必须返回一个迭代器对象。
  2. 迭代器对象需要有 next() 方法: next() 方法必须返回一个包含 valuedone 属性的对象。

代码示例:

const myIterable = {
  data: [1, 2, 3],
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        if (index < this.data.length) {
          return { value: this.data[index++], done: false };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
};

for (const element of myIterable) {
  console.log(element); // 依次打印 1, 2, 3
}

这个例子中,我们定义了一个 myIterable 对象,它有一个 data 属性,存储了我们要迭代的数据。 然后,我们实现了 Symbol.iterator 方法,它返回一个迭代器对象。 这个迭代器对象有一个 next() 方法,每次调用 next() 方法,它都会返回 data 数组中的下一个元素,直到数组的末尾。

四、 Generator:更优雅的迭代器创建方式

手动实现迭代器对象还是有点麻烦的,特别是当你的数据结构比较复杂的时候。 这时候,Generator 就派上用场了。

Generator 是一种特殊的函数,它可以让你更轻松地创建迭代器。 它使用 function* 语法来定义,并且可以使用 yield 关键字来暂停函数的执行,并返回一个值。

代码示例:

function* myGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

const iterator = myGenerator();

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

看到了没? 我们用 function* 定义了一个 myGenerator 函数。 在函数内部,我们使用 yield 关键字来返回一个值。 每次调用 iterator.next(),函数都会从上次 yield 的地方继续执行,直到遇到下一个 yield 或者函数结束。

Generator 创建可迭代对象:

const myIterable = {
  data: [1, 2, 3],
  [Symbol.iterator]: function*() {
    for (const element of this.data) {
      yield element;
    }
  }
};

for (const element of myIterable) {
  console.log(element); // 依次打印 1, 2, 3
}

这个例子中,我们用 Generator 函数来实现 Symbol.iterator 方法。 代码更加简洁易懂了。

五、惰性求值:按需计算,节省资源

Generator 的一个重要特性就是支持惰性求值。 这意味着,只有在你需要用到某个值的时候,才会去计算它。 这在处理大量数据或者计算密集型任务时非常有用,可以节省大量的资源。

代码示例:

function* infiniteSequence() {
  let n = 0;
  while (true) {
    yield n++;
  }
}

const iterator = infiniteSequence();

console.log(iterator.next()); // { value: 0, done: false }
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
// ...

这个例子中,我们定义了一个 infiniteSequence 生成器函数,它可以生成一个无限的数字序列。 如果我们一次性把所有的数字都计算出来,那肯定会爆内存。 但是,由于 Generator 支持惰性求值,所以只有在我们需要用到某个数字的时候,才会去计算它。

惰性求值的优势:

  • 节省内存: 不需要一次性加载所有数据到内存中。
  • 提高性能: 只计算需要用到的数据,避免不必要的计算。
  • 可以处理无限序列: 可以处理无法一次性加载到内存中的无限序列。

*六、 `yield`:代理迭代,化繁为简**

yield* 是一种特殊的 yield 语法,它可以让你把一个迭代器代理给另一个迭代器。 简单来说,就是在一个 Generator 函数里,你可以用 yield* 来迭代另一个可迭代对象。

代码示例:

function* generator1() {
  yield 1;
  yield 2;
}

function* generator2() {
  yield* generator1(); // 代理 generator1 的迭代
  yield 3;
  yield 4;
}

const iterator = generator2();

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: 4, done: false }
console.log(iterator.next()); // { value: undefined, done: true }

这个例子中,generator2 使用 yield* generator1() 来代理 generator1 的迭代。 这意味着,generator2 会先迭代 generator1 产生的所有值,然后再继续执行自己的代码。

yield* 可以让你更方便地组合多个迭代器,构建更复杂的迭代逻辑。

七、实际应用场景:

  • 处理大型文件: 可以逐行读取大型文件,而不需要一次性加载整个文件到内存中。
  • 实现无限滚动加载: 可以根据用户的滚动位置,动态加载数据,而不需要一次性加载所有数据。
  • 构建复杂的数据流: 可以把多个 Generator 函数组合起来,构建复杂的数据处理流程。
  • 异步编程: Generator 函数可以和 Promise 结合使用,实现更优雅的异步编程。 (这个比较高级,以后有机会再细说)

八、 总结:

特性 Iterator Generator
定义 具有 next() 方法的对象 一种特殊的函数,使用 function* 定义
创建方式 手动实现 next() 方法 使用 yield 关键字暂停和恢复函数执行
主要用途 提供一种统一的访问集合元素的方式 更简洁地创建迭代器,支持惰性求值
惰性求值 不支持 支持
yield* 不支持 支持,用于代理迭代

总而言之,IteratorGenerator 是 JS 中非常强大的特性,它们可以让你更轻松地处理各种数据结构,并且可以提高代码的效率和可读性。 希望今天的讲解能帮助大家更好地理解和运用这两个特性。

好了,今天的讲座就到这里。 大家还有什么问题吗? 没有的话,就散会啦! 记得回去好好练习哦! 咱们下期再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注