各位观众,晚上好!我是今晚的主讲人,咱们今晚就来聊聊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()
方法,迭代器对象都会返回一个包含value
和done
属性的对象。value
: 表示当前迭代的值。done
: 是一个布尔值,表示迭代是否完成。true
表示迭代结束,false
表示还有更多值可以迭代。
二、Iterable协议的“三板斧”
总结一下,Iterable协议就是一个对象需要具备以下三个要素,我把它戏称为“三板斧”:
Symbol.iterator
属性: 必须存在,且是一个函数。Symbol.iterator
函数返回值: 必须是一个迭代器对象。- 迭代器对象必须有
next()
方法:next()
方法返回一个包含value
和done
属性的对象。
三、for...of
循环的工作原理
有了Iterable协议的铺垫,for...of
循环的工作原理就变得清晰了。 当我们使用for...of
循环遍历一个对象时,JavaScript引擎会做以下几件事:
- 检查对象是否实现了Iterable协议: 引擎会查找对象是否具有
Symbol.iterator
属性。如果没有,就会报错,告诉你这个对象不能被for...of
循环遍历。 - 调用
Symbol.iterator
方法: 如果对象实现了Iterable协议,引擎就会调用它的Symbol.iterator
方法,获取一个迭代器对象。 - 循环调用
next()
方法: 引擎会不断地调用迭代器对象的next()
方法,直到next()
方法返回的对象的done
属性为true
为止。 - 获取
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协议的出现,是为了解决以下几个问题:
- 统一遍历方式: Iterable协议提供了一种统一的遍历方式,无论对象内部的数据结构如何,只要实现了Iterable协议,就可以使用
for...of
循环进行遍历。 这使得代码更加简洁和易于维护。 - 抽象迭代过程: Iterable协议将迭代过程抽象出来,使得我们可以专注于处理数据,而不需要关心迭代的细节。 就像开车一样,我们只需要知道方向盘和油门怎么用,不需要了解发动机的工作原理。
- 支持自定义迭代: Iterable协议允许我们自定义迭代过程,例如,我们可以实现一个生成斐波那契数列的Iterable对象,或者实现一个遍历二叉树的Iterable对象。 这为我们提供了更大的灵活性。
- 与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()
方法,该方法返回一个包含value
和done
属性的对象。 for...of
循环使用Iterable协议来遍历对象。- Generator函数可以用来创建迭代器。
掌握Iterable协议,可以帮助我们编写更简洁、更高效、更灵活的JavaScript代码。
希望今天的讲座对大家有所帮助! 谢谢大家!