JavaScript内核与高级编程之:`JavaScript`的`Iterable`协议:`for…of`循环的底层实现。

各位观众,晚上好!我是今晚的主讲人,咱们今晚就来聊聊JavaScript里一个看似简单,实则蕴含深刻哲理的东东——Iterable协议,以及它背后支撑的for...of循环。 别看for...of循环平时用得挺顺手,但你知道它是怎么工作的吗?今天咱们就扒开它的皮,看看里面藏着什么秘密。

一、什么是Iterable协议?

Iterable协议,说白了,就是JavaScript里定义的一种规范,一种标准。任何对象,只要遵守了这个协议,就可以使用for...of循环进行遍历。 就像一个插座,只要电器插头符合插座的规格,就能插上去用。Iterable协议就是这个“插座规格”,而可以被for...of遍历的对象,就是那些符合规格的“电器插头”。

那么,这个“插座规格”到底是什么呢? Iterable协议要求一个对象必须提供一个名为Symbol.iterator方法。 这个方法执行后,需要返回一个迭代器对象(Iterator)。

听起来有点绕? 没关系,咱们一步一步来。

  • Symbol.iterator: 这是一个特殊的Symbol值,用来表示一个对象是否实现了Iterable协议。 记住,这是一个属性名,它的值必须是一个函数!

  • 迭代器对象 (Iterator): 这个对象是遍历的核心。它必须提供一个名为next()的方法。每次调用next()方法,迭代器对象都会返回一个包含valuedone属性的对象。

    • value: 表示当前迭代的值。
    • done: 是一个布尔值,表示迭代是否完成。true表示迭代结束,false表示还有更多值可以迭代。

二、Iterable协议的“三板斧”

总结一下,Iterable协议就是一个对象需要具备以下三个要素,我把它戏称为“三板斧”:

  1. Symbol.iterator属性: 必须存在,且是一个函数。
  2. Symbol.iterator函数返回值: 必须是一个迭代器对象。
  3. 迭代器对象必须有next()方法: next()方法返回一个包含valuedone属性的对象。

三、for...of循环的工作原理

有了Iterable协议的铺垫,for...of循环的工作原理就变得清晰了。 当我们使用for...of循环遍历一个对象时,JavaScript引擎会做以下几件事:

  1. 检查对象是否实现了Iterable协议: 引擎会查找对象是否具有Symbol.iterator属性。如果没有,就会报错,告诉你这个对象不能被for...of循环遍历。
  2. 调用Symbol.iterator方法: 如果对象实现了Iterable协议,引擎就会调用它的Symbol.iterator方法,获取一个迭代器对象。
  3. 循环调用next()方法: 引擎会不断地调用迭代器对象的next()方法,直到next()方法返回的对象的done属性为true为止。
  4. 获取value值: 每次调用next()方法,引擎都会获取返回对象的value属性,并将其赋值给for...of循环中的变量。

四、代码示例:手动实现一个Iterable对象

光说不练假把式,咱们来写一段代码,手动实现一个Iterable对象。 假设我们要创建一个名为Range的对象,它可以生成指定范围内的数字序列。

class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  [Symbol.iterator]() {
    let currentValue = this.start;
    const that = this; // 保存this上下文,防止在迭代器中使用时this指向改变

    return {
      next() {
        if (currentValue <= that.end) {
          return { value: currentValue++, done: false };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
}

const range = new Range(1, 5);

for (const num of range) {
  console.log(num); // 输出 1 2 3 4 5
}

// 也可以手动调用迭代器
const iterator = range[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: 4, done: false}
console.log(iterator.next()); // {value: 5, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

在这个例子中,我们定义了一个Range类,并实现了Symbol.iterator方法。 这个方法返回一个迭代器对象,该迭代器对象具有next()方法。 每次调用next()方法,迭代器对象都会返回一个包含当前数字和迭代状态的对象。

五、原生Iterable对象

JavaScript内置了很多实现了Iterable协议的对象,例如:

  • Array: 数组是最常见的Iterable对象。
  • String: 字符串也可以被for...of循环遍历。
  • Map: Map对象可以遍历键值对。
  • Set: Set对象可以遍历集合中的元素。
  • arguments: 函数的arguments对象(注意,它不是真正的数组,但可以被for...of遍历)。
  • NodeList: DOM API返回的NodeList对象。
  • TypedArray: 比如Int8Array, Uint8Array等等。

这些对象都内置了Symbol.iterator方法,可以直接使用for...of循环进行遍历。

const arr = [1, 2, 3];
for (const item of arr) {
  console.log(item); // 输出 1 2 3
}

const str = "hello";
for (const char of str) {
  console.log(char); // 输出 h e l l o
}

const map = new Map([["a", 1], ["b", 2]]);
for (const [key, value] of map) {
  console.log(key, value); // 输出 a 1   b 2
}

const set = new Set([1, 2, 3]);
for (const item of set) {
  console.log(item); // 输出 1 2 3
}

六、为什么要有Iterable协议?

你可能会问,为什么JavaScript要搞这么一套复杂的Iterable协议呢?直接用下标访问数组元素不香吗?

Iterable协议的出现,是为了解决以下几个问题:

  1. 统一遍历方式: Iterable协议提供了一种统一的遍历方式,无论对象内部的数据结构如何,只要实现了Iterable协议,就可以使用for...of循环进行遍历。 这使得代码更加简洁和易于维护。
  2. 抽象迭代过程: Iterable协议将迭代过程抽象出来,使得我们可以专注于处理数据,而不需要关心迭代的细节。 就像开车一样,我们只需要知道方向盘和油门怎么用,不需要了解发动机的工作原理。
  3. 支持自定义迭代: Iterable协议允许我们自定义迭代过程,例如,我们可以实现一个生成斐波那契数列的Iterable对象,或者实现一个遍历二叉树的Iterable对象。 这为我们提供了更大的灵活性。
  4. 与Generator函数结合: Iterable协议可以与Generator函数结合使用,创建更强大的迭代器。 后面我们会讲到Generator,这里先埋个伏笔。

七、Iterable vs Iterator

这两个概念很容易混淆,咱们再来区分一下。

特性 Iterable Iterator
定义 表示可迭代的,具有Symbol.iterator方法的对象 迭代器对象,具有next()方法的对象
作用 提供迭代器对象 负责实际的迭代过程
关系 Iterable对象返回Iterator对象 Iterator对象是Iterable对象迭代的具体实现
例子 数组、字符串、Map、Set等 Range类中的迭代器对象

可以把Iterable看作是一个“可迭代的容器”,而Iterator则是这个容器里的“搬运工”。

八、Generator函数与Iterable

Generator函数是ES6引入的一个强大的特性,它可以用来创建迭代器。 Generator函数使用function*语法定义,并且可以使用yield关键字来暂停函数的执行,并返回一个值。

function* numberGenerator(start, end) {
  let currentValue = start;
  while (currentValue <= end) {
    yield currentValue++;
  }
}

const generator = numberGenerator(1, 5);

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

// 也可以使用for...of循环
for (const num of numberGenerator(1, 5)) {
  console.log(num); // 输出 1 2 3 4 5
}

Generator函数返回一个迭代器对象,因此可以直接使用for...of循环进行遍历。 这使得创建Iterable对象变得更加简单。

九、自定义Iterable对象的应用场景

自定义Iterable对象有很多应用场景,例如:

  • 处理大型数据集: 如果数据集非常大,一次性加载到内存中可能会导致性能问题。 可以使用Iterable对象来分批加载数据,避免内存溢出。
  • 实现惰性计算: Iterable对象可以实现惰性计算,只在需要时才计算值。 这可以提高程序的性能,尤其是在处理复杂计算时。
  • 创建无限序列: 可以使用Iterable对象来创建无限序列,例如,生成斐波那契数列或素数序列。

十、总结

今天我们深入探讨了JavaScript的Iterable协议,以及它背后的for...of循环的工作原理。 记住以下几个关键点:

  • Iterable协议是一种规范,用于定义可迭代对象。
  • Iterable对象必须提供一个Symbol.iterator方法,该方法返回一个迭代器对象。
  • 迭代器对象必须提供一个next()方法,该方法返回一个包含valuedone属性的对象。
  • for...of循环使用Iterable协议来遍历对象。
  • Generator函数可以用来创建迭代器。

掌握Iterable协议,可以帮助我们编写更简洁、更高效、更灵活的JavaScript代码。

希望今天的讲座对大家有所帮助! 谢谢大家!

发表回复

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