Iterator 和 for...of 的关系是什么?
简化版
Iterator 是统一遍历协议,for...of 会读取对象的 Symbol.iterator 方法,拿到迭代器后不断调用 next(),直到 { done: true } 结束。
详细版
可迭代对象必须实现 Symbol.iterator 方法。
这个方法返回一个迭代器对象,迭代器对象需要有 next() 方法。
const arr = [1, 2, 3];
const iterator = arr[Symbol.iterator]();
iterator.next(); // { value: 1, done: false }
for...of 消费的是迭代协议,不是下标。
数组、字符串、Map、Set、arguments、NodeList 等都可以被遍历,是因为它们实现了迭代协议。
普通对象默认不能 for...of,除非手动实现 Symbol.iterator。
完整版教学
一、Iterator 的目标是统一遍历入口
ES6 之前,不同数据结构的遍历方式不统一。
数组用下标,普通对象用 for...in 或 Object.keys,类数组对象又要借方法转换。
Iterator 协议提供一个共同抽象:只要对象能按约定吐出下一个值,它就可以被统一消费。
数据结构
-> Symbol.iterator()
-> iterator.next()
-> { value, done }
这个抽象让 for...of、展开运算符、解构赋值、Array.from 都能复用同一套遍历能力。
所以它不是单独服务于循环语法,而是 ES6 数据消费模型的一部分。
二、可迭代对象和迭代器不是同一个概念
可迭代对象是拥有 Symbol.iterator 方法的对象。
迭代器是拥有 next() 方法、能一步步返回结果的对象。
数组本身是可迭代对象,但数组本身不是它自己的迭代器。
const arr = [10, 20];
console.log(typeof arr[Symbol.iterator]); // "function"
const it = arr[Symbol.iterator]();
console.log(it.next()); // { value: 10, done: false }
console.log(it.next()); // { value: 20, done: false }
console.log(it.next()); // { value: undefined, done: true }
用数字例子看更清楚:长度为 2 的数组通常会经历 3 次 next()。
前 2 次给值,第 3 次告诉消费者结束。
三、for…of 背后就是不断调用 next
for...of 的语义可以近似理解成:先取迭代器,再循环调用 next()。
只要 done 不是 true,就把 value 交给循环变量。
当 done 为 true,循环结束。
for (const item of [1, 2, 3]) {
console.log(item);
}
近似流程:
取 iterable[Symbol.iterator]()
-> next() 得到 1
-> next() 得到 2
-> next() 得到 3
-> next() done=true
-> 结束
这解释了为什么 for...of 可以遍历 Set。
Set 没有数字下标,但它实现了迭代协议。
四、for…of 和 for…in 的差别很高频
for...in 遍历的是对象可枚举属性名,常用于对象属性枚举。
for...of 遍历的是可迭代对象吐出的值,常用于数组、字符串、Map、Set。
把两者混用,是面试和项目里都常见的问题。
| 维度 | for...in | for...of |
|---|---|---|
| 关注点 | 属性名 key | 迭代值 value |
| 依赖机制 | 可枚举属性 | Iterator 协议 |
| 数组结果 | 下标字符串 | 元素值 |
| 普通对象 | 默认可用 | 默认不可用 |
const arr = ['a', 'b'];
for (const k in arr) console.log(k); // "0", "1"
for (const v of arr) console.log(v); // "a", "b"
数组遍历业务值时优先考虑 for...of。
枚举对象属性时再考虑 for...in,并注意原型链属性过滤。
五、自定义可迭代对象怎么写
普通对象默认不能 for...of,但可以通过实现 Symbol.iterator 变成可迭代对象。
关键是返回一个带 next() 的对象。
每次 next() 都要返回 { value, done }。
const range = {
start: 1,
end: 3,
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
};
console.log([...range]); // [1, 2, 3]
这个 range 对象没有数组下标,却能被展开运算符消费。
原因不是展开运算符认识 range,而是它认识迭代协议。
六、Generator 是创建迭代器的语法糖
手写 next() 比较繁琐。
Generator 函数能自动返回一个迭代器,每次 yield 对应一次可产出的值。
所以 Generator 常被用来实现自定义遍历。
function* range(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
console.log([...range(1, 3)]); // [1, 2, 3]
记忆钩子:
for...of不关心你是不是数组,它只问你有没有Symbol.iterator。
从工程角度看,自定义迭代适合“按需产生值”的场景。 例如分页数据、树结构遍历、流式解析都可以用迭代协议隐藏内部细节。
七、常见误区与追问
- 误区:能用下标访问就一定能
for...of。for...of看的是迭代协议,不是下标能力;类数组对象也要看是否实现Symbol.iterator。 - 误区:普通对象可以直接
for...of。 普通对象默认没有Symbol.iterator,需要手动实现或转成Object.keys/values/entries。 - 误区:
for...in和for...of都适合遍历数组值。for...in得到的是属性名,数组业务值更适合for...of。 - 追问:展开运算符为什么能展开 Set? Set 实现了迭代协议,展开运算符会读取它的迭代器并逐个取值。
- 追问:Iterator 的
done有什么作用? 它告诉消费者遍历是否结束;没有结束信号,循环就无法可靠停止。 - 追问:Generator 和 Iterator 是什么关系? Generator 返回的对象符合迭代器协议,是写迭代器的一种简洁方式。
八、加强记忆
把 Iterator 理解成“统一取下一个值的接口”:可迭代对象提供 Symbol.iterator,迭代器提供 next(),for...of 负责循环消费。答题时用数组的 3 次 next() 例子解释协议,再对比 for...in,基本就能覆盖主要追问。