🔄 深入理解 JavaScript:什么是可迭代对象 (Iterable)?
🤔 为什么需要"可迭代"?
在 ES6 之前,我们遍历数据主要靠 for 循环(基于索引)或 forEach。但这种方式有两个痛点:
- 不通用:数组能遍历,但字符串、DOM 节点列表、Map、Set 的遍历方式各不相同。
- 不灵活:无法自定义遍历逻辑(比如我想倒序遍历,或者只遍历偶数项)。
为了解决这个问题,ES6 引入了 迭代器 (Iterator) 和 可迭代对象 (Iterable) 的概念,统一了数据访问的接口。
通俗比喻 :
想象你去一家自助餐厅吃饭。
- 普通对象:像是一堆散落在桌子上的菜,没有顺序,你只能指着说"我要那个",不能按顺序一个个拿。
- 可迭代对象 :像是传送带上的寿司。传送带有一个机制 (迭代器),让你可以一个一个地按顺序获取食物,直到吃完为止。
Symbol.iterator就是那个"启动传送带的按钮"。只要一个对象身上有这个按钮,它就是"可迭代的"。
📂 目录
- [🛠️ 核心概念:谁是谁?](#🛠️ 核心概念:谁是谁?)
- [🔍 底层原理:两个协议](#🔍 底层原理:两个协议)
- [💻 代码实战:判断与使用](#💻 代码实战:判断与使用)
- [✍️ 手写实现:让普通对象可迭代](#✍️ 手写实现:让普通对象可迭代)
- [⚠️ 常见误区:Array-like vs Iterable](#⚠️ 常见误区:Array-like vs Iterable)
- [💡 总结](#💡 总结)
1. 🛠️ 核心概念:谁是谁?
很多初学者容易混淆这两个术语,我们先厘清定义:
✅ 可迭代对象 (Iterable)
- 定义 :实现了 可迭代协议 的对象。
- 特征 :对象内部必须有一个键为
Symbol.iterator的方法,该方法返回一个迭代器对象。 - 常见例子 :
Array,String,Map,Set,arguments,NodeList。
✅ 迭代器 (Iterator)
- 定义 :实现了 迭代器协议 的对象。
- 特征 :拥有一个
next()方法,每次调用返回一个包含value和done的对象。 - 作用:它是真正的"工人",负责逐个吐出数据。
简单关系 :
可迭代对象 是工厂,迭代器 是工厂派出的快递员。当你使用
for...of时,JS 引擎会自动调用工厂的Symbol.iterator拿到快递员,然后一直喊next()直到送完货。
2. 🔍 底层原理:两个协议
协议一:可迭代协议 (Iterable Protocol)
只要对象拥有 [@@iterator] 方法(即 Symbol.iterator 属性),它就是可迭代的。
javascript
const arr = [1, 2, 3];
// 检查是否有 Symbol.iterator 方法
console.log(typeof arr[Symbol.iterator]); // "function"
协议二:迭代器协议 (Iterator Protocol)
Symbol.iterator 方法返回的对象,必须满足以下条件:
- 拥有
next()方法。 next()返回一个对象{ value: any, done: boolean }。value: 当前产出的值。done: 是否遍历结束(true表示结束)。
3. 💻 代码实战:判断与使用
场景一:哪些是原生的可迭代对象?
javascript
// ✅ 数组
[1, 2, 3][Symbol.iterator]; // function
// ✅ 字符串
"hello"[Symbol.iterator]; // function
// ✅ Map
new Map()[Symbol.iterator](
// function
// ❌ 普通对象
{},
)[Symbol.iterator]; // undefined -> 不可迭代!
场景二:for...of 的本质
当你写这段代码时:
javascript
for (const item of [1, 2, 3]) {
console.log(item);
}
JavaScript 引擎实际上在执行以下操作:
javascript
const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator](); // 1. 获取迭代器
let result = iterator.next(); // 2. 第一次 next()
while (!result.done) {
// 3. 如果没做完
console.log(result.value); // 4. 处理值
result = iterator.next(); // 5. 继续下一个
}
4. ✍️ 手写实现:让普通对象可迭代
这是面试的高频考点:如何让一个普通对象支持 for...of?
假设我们有一个范围对象 Range,我们希望它能遍历出从 start 到 end 的数字。
javascript
const range = {
start: 1,
end: 5,
};
// ❌ 直接遍历会报错:range is not iterable
// for (let num of range) { ... }
// ✅ 改造:添加 Symbol.iterator 方法
range[Symbol.iterator] = function () {
let current = this.start;
const last = this.end;
// 返回一个符合"迭代器协议"的对象
return {
next() {
if (current <= last) {
return { value: current++, done: false };
} else {
return { value: undefined, done: true };
}
},
};
};
// 现在可以愉快地遍历了!
for (let num of range) {
console.log(num); // 1, 2, 3, 4, 5
}
// 也可以使用展开运算符
console.log([...range]); // [1, 2, 3, 4, 5]
关键点 :
Symbol.iterator函数必须返回一个新的迭代器对象。这样即使嵌套遍历或多次遍历,状态也不会冲突。
5. ⚠️ 常见误区:Array-like vs Iterable
很多"类数组对象"(Array-like)不是可迭代对象,这经常导致 Bug。
❌ 误区:arguments 和 NodeList
在旧版浏览器或某些环境下,document.querySelectorAll 返回的 NodeList 可能没有 Symbol.iterator(虽然现代浏览器大多已支持,但需警惕兼容性)。
javascript
const divs = document.querySelectorAll("div");
// ❌ 在某些旧环境或特定对象上可能失效
// for (let div of divs) { ... }
// ✅ 安全做法:使用 Array.from 转换
const divArray = Array.from(divs);
divArray.forEach((div) => console.log(div));
✅ 最佳实践:如何安全地遍历?
如果你不确定一个对象是否可迭代,可以使用以下工具函数检测:
javascript
function isIterable(obj) {
// 检查 null/undefined
if (obj == null) return false;
// 检查是否有 Symbol.iterator 方法且类型为函数
return typeof obj[Symbol.iterator] === "function";
}
console.log(isIterable([])); // true
console.log(isIterable({})); // false
console.log(isIterable("")); // true
6. 💡 总结
| 概念 | 说明 | 关键方法/属性 |
|---|---|---|
| 可迭代对象 | 能被 for...of 遍历的对象 |
obj[Symbol.iterator] |
| 迭代器 | 负责逐个返回数据的对象 | iterator.next() |
| next() 返回值 | 包含值和状态的对象 | { value: ..., done: ... } |
| 原生可迭代 | Array, String, Map, Set, arguments 等 | - |
| 非可迭代 | 普通 Object {} |
- |
🚀 博主寄语 :
可迭代协议是 JavaScript 统一数据访问方式的伟大尝试。
它不仅让
for...of成为可能,还支撑起了生成器 (Generator)、异步迭代器 (Async Iterator) 等高级特性。记住口诀 :
想要遍历用
for...of,对象必须有
iterator。
next方法吐数据,
done为真停脚步。
希望这篇文档能帮你彻底搞懂可迭代对象!如果有疑问,欢迎在评论区留言。👇
喜欢这篇文章吗?记得点赞、收藏、转发哦! ❤️