1. 从“遍历三代人”说起:for-of 到底解决了什么问题
写 JavaScript 写了几年,回头看看数组遍历这件事,真算得上一部演化史。最早我们写 for (var i = 0; i < arr.length; i++),条件判断、步长递增、索引取值,三件套一次写齐,繁琐但踏实。后来 ES5 带来了 forEach,写法是简洁了,可它有一个先天缺陷:break 和 continue 在里面根本不能用,想中途跳出循环?门都没有。再后来 jQuery 的 $.each、underscore 的 _.each,本质上都是回调函数的包装,終归不够“原生”。
for-of 在 ES6 里登场,就是来终结这个尴尬局面的。它既能像 for 一样用 break、continue、return 控制循环流程,写法上又比传统 for 简洁得多。但如果你以为 for-of 只是“数组遍历的语法糖”,那就太小看它了——它背后的迭代器协议,才是真正让 JavaScript 遍历能力脱胎换骨的东西。这篇文章我就从使用、原理、坑点三个层面,把 for-of 掰开揉碎讲清楚。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先看用法:for-of 基本语法与常见数据类型遍历
2.1 基本语法长什么样
for-of 的语法非常简洁:
javascript复制for (const value of iterable) {
// 每次循环 value 就是当前元素
}
这里有两个细节值得注意。第一是 const:循环变量用 const 声明完全合法,因为每次迭代都是一个新的绑定,这跟 for 循环里 var i 是同一个变量、会相互覆盖有本质区别。当然你想在循环体内重新赋值给这个变量,那就得用 let,但实践中绝大多数场景根本不需要改它。
第二是右侧的 iterable。这个词是“可迭代对象”的意思,数组、字符串、Set、Map、arguments、DOM 的 NodeList、Generator 对象等,都满足这个条件。for-of 会自动调用这个对象的迭代器接口,逐个取出里面的值。
2.2 数组遍历:最基础的场景
数组是 for-of 最典型的应用场景,对比一下三种写法你就能直观感受到差异:
javascript复制const arr = [10, 20, 30];
// 传统 for
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
// forEach
arr.forEach(item => console.log(item));
// for-of
for (const item of arr) {
console.log(item);
}
for-of 不需要索引、不需要回调函数、不需要关心 this 绑定,直接拿值。代码意图一目了然:我就是要遍历数组里的每个元素。
2.3 字符串遍历:按码元还是按字符?这是个问题
for-of 遍历字符串时,有一个隐性优势经常被忽略。直接用索引取字符串某个位置的字符,取到的是 UTF-16 码元,遇到 emoji 这类超出基本多语言平面的字符就会出问题:
javascript复制const str = 'a😀b';
console.log(str.length); // 4,注意不是 3
console.log(str[1]); // '\ud83d',半截乱码
for (const char of str) {
console.log(char); // 依次输出 'a'、'😀'、'b'
}
原因在于字符串的迭代器是按“码点”而不是按“码元”来遍历的。像 😀 这样需要两个码元表示的字符,在 for-of 里会被正确地当做一个字符处理。如果你的业务里涉及 emoji、生僻字、或者多语言文本处理,这一点能帮你省掉不少麻烦。
2.4 Set 与 Map:取值方式完全不同于数组
Set 遍历时,取到的是集合中的值本身;Map 遍历时,取到的是一个 [key, value] 数组:
javascript复制const set = new Set(['a', 'b', 'c']);
for (const item of set) {
console.log(item); // 依次输出 'a' 'b' 'c'
}
const map = new Map([['name', '张三'], ['age', 30]]);
for (const entry of map) {
console.log(entry); // 依次输出 ['name', '张三']、['age', 30]
}
// 更常用的写法:配合解构
for (const [key, value] of map) {
console.log(key, value);
}
第二种写法配合数组解构,是遍历 Map 最优雅的方式之一。你不需要像以前那样先 Array.from(map) 转成数组再遍历,直接上手就行。
2.5 arguments、NodeList 等类数组对象也能遍历
以前想把 arguments 转成真数组,得写 Array.prototype.slice.call(arguments) 或者 Array.from(arguments)。现在 for-of 可以直接遍历它:
javascript复制function sum() {
let total = 0;
for (const num of arguments) {
total += num;
}
return total;
}
sum(1, 2, 3, 4); // 10
浏览器里的 NodeList(比如 document.querySelectorAll 的返回值)同样可以直接用 for-of 遍历,不需要再 Array.from 转一遍。
3. 幕后机制:迭代器协议与 Symbol.iterator
3.1 可迭代协议和迭代器协议
for-of 不是魔法,它依赖的是 ES6 引入的两套协议:可迭代协议和迭代器协议。
可迭代协议要求对象必须实现 Symbol.iterator 方法。这个方法返回一个迭代器对象。迭代器协议要求这个对象必须有一个 next() 方法,每次调用 next() 都返回一个 { value: 某个值, done: false } 格式的对象,当遍历结束时返回 { value: undefined, done: true }。
for-of 的运行时行为是这样的:
- 调用可迭代对象的
Symbol.iterator方法,拿到迭代器; - 每次循环调用迭代器的
next(); - 判断返回对象的
done属性,true则结束循环; - 否则将
value赋给循环变量,执行循环体。
手动模拟一下就是:
javascript复制const arr = ['a', 'b', 'c'];
const iterator = arr[Symbol.iterator]();
while (true) {
const result = iterator.next();
if (result.done) break;
console.log(result.value);
}
你甚至可以完全抛开 for-of,直接用迭代器来手动控制遍历过程。这种灵活性后面讲自定义迭代器时会体现出来。
3.2 原生可迭代对象一览
JavaScript 内置的可迭代对象包括这些:
ArrayStringSetMaparguments对象NodeListTypedArray(如Int32Array、Uint8Array)Generator对象
普通对象 {} 不是可迭代的,直接对它用 for-of 会抛出 TypeError: object is not iterable。这是我见过初学者最爱踩的坑之一。
3.3 如何判断一个对象是否可迭代
想知道某个对象能不能用 for-of,最简单的办法是检查它有没有 Symbol.iterator 方法:
javascript复制console.log(typeof arr[Symbol.iterator]); // 'function'
console.log(typeof obj[Symbol.iterator]); // 'undefined'
也可以写一个小工具函数:
javascript复制function isIterable(obj) {
return obj != null && typeof obj[Symbol.iterator] === 'function';
}
3.4 自定义迭代器:让普通对象也能被 for-of 遍历
普通对象不默认可迭代,但我们可以手动给它挂上 Symbol.iterator 方法。举个例子,一个简单的范围对象,想让它能被 for-of 遍历:
javascript复制const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
if (current <= last) {
return { value: current++, done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
};
for (const num of range) {
console.log(num); // 依次输出 1 2 3 4 5
}
注意 [Symbol.iterator]() 方法里的 this 指向 range 对象本身,而返回的迭代器对象里的 next 是普通函数,它内部访问的 current 和 last 是通过闭包捕获的。这种写法是迭代器实现的标准模板。
4. 对比分析:for-of 与 for-in、forEach、传统 for 的取舍之道
4.1 四者核心区别一览表
很多面试场合喜欢问 for-of 和 for-in 的区别。我直接给一张对比表,建议收藏:
| 对比维度 | 传统 for | forEach | for-in | for-of |
|---|---|---|---|---|
| 遍历对象类型 | 数组等有索引的 | 数组 | 对象可枚举属性 | 可迭代对象 |
| 能否 break/continue | 可以 | 不可以 | 可以 | 可以 |
| 能不能拿到索引 | 直接拿 | 第二个回调参数 | 拿的是键名 | 拿的是值,需要配合 entries |
| 遍历数组元素值 | 需要 arr[i] | 回调第一参数 | 拿到的是索引字符串 | 直接拿值 |
| 遍历顺序是否可靠 | 可靠 | 可靠 | 不可靠 | 由迭代器决定,一般可靠 |
| 对象 {} 是否能用 | 不能 | 不能 | 可以 | 不能 |
4.2 为什么 for-in 遍历数组是个馊主意
for-in 的本意是遍历对象的可枚举属性,用来遍历数组会引入两个问题。第一,取到的是索引字符串而不是值,你得 arr[key] 再去取;第二,原型链上自定义的可枚举属性也会被遍历出来。
javascript复制const arr = ['a', 'b', 'c'];
Array.prototype.custom = 'x';
for (const key in arr) {
console.log(key); // 输出 '0'、'1'、'2'、'custom'
}
custom 也会被遍历到。这个问题在老的 JavaScript 库里经常引发 bug,也是面试题里的经典陷阱。for-of 只遍历可迭代对象提供的元素,完全规避了原型链问题。
4.3 什么时候该用 forEach,什么时候该用 for-of
我的经验是一个简单的判断标准:如果你需要在循环中使用 break / continue / return(在函数里),必须用 for-of 或传统 for;如果你想写函数式链式调用(比如先 filter 再 map 再 forEach),forEach 更适合嵌入链中;如果对性能极度敏感且遍历超大数组,传统 for 或者 for-of 都行,但别用 forEach,因为函数调用有额外开销。
javascript复制// 场景一:找到第一个大于 10 的数就停止
const nums = [3, 15, 8, 20];
let target;
for (const num of nums) {
if (num > 10) {
target = num;
break; // forEach 做不到
}
}
// 场景二:链式调用
nums.filter(n => n > 5).forEach(n => console.log(n));
4.4 性能实测:for-of 到底慢不慢
很多人担心 for-of 比传统 for 慢,毕竟它走的是迭代器协议,多了一层函数调用。这个担心有一定道理,但要看具体场景。
我实测过十万元素数组的遍历,结果是这样的(以传统 for 为基准):
- 传统 for:约 1.0x 耗时
- for-of:约 1.2x ~ 1.5x 耗时
- forEach:约 1.5x ~ 2.0x 耗时
也就是说,for-of 确实比传统 for 略慢,但慢得非常有限。在绝大多数业务场景里,这个差距完全可以忽略不计。真正的瓶颈是循环体内的逻辑,而不是遍历方式本身。如果你遍历的是一个百万级以上的数组,并且每一轮操作都很轻量,可以考虑优化成传统 for;其他情况,优先选 for-of,清晰度带来的维护成本优势远比那零点几毫秒重要。
5. 进阶技巧:解构、entries、性能陷阱与跳出循环
5.1 配合解构赋值:Map 遍历的正确姿势
前面提到了 Map 配合解构的写法,这里再展开一下。for-of 取出的每个条目是 [key, value] 数组,直接解构的话代码很自然:
javascript复制const userRoles = new Map([
['zhangsan', 'admin'],
['lisi', 'editor'],
['wangwu', 'viewer']
]);
for (const [username, role] of userRoles) {
console.log(`${username}: ${role}`);
}
如果你用 Object.entries 把普通对象转成数组,同样可以享受这种解构便利:
javascript复制const user = { name: '张三', age: 30 };
for (const [key, value] of Object.entries(user)) {
console.log(key, value);
}
5.2 entries()、keys()、values():要值还是要索引?
数组有三个内置方法可以配合 for-of:
javascript复制const arr = ['a', 'b', 'c'];
// 只要值
for (const value of arr.values()) {
console.log(value); // 'a' 'b' 'c'
}
// 只要索引
for (const index of arr.keys()) {
console.log(index); // 0 1 2
}
// 索引和值都要
for (const [index, value] of arr.entries()) {
console.log(index, value); // 0 'a'、1 'b'、2 'c'
}
这里要特别说明:如果你直接 for (const v of arr),其实等同于 for (const v of arr.values()),两者拿到的都是值。需要索引的场景,优先用 entries() 而不是去遍历索引后再取值。
5.3 break、continue、return:for-of 的自由度
for-of 不像 forEach 那样限制你的流程控制。break 跳出整个循环,continue 跳过当前迭代进入下一个,在函数内部还可以用 return 直接结束函数并返回值:
javascript复制function findFirstEven(nums) {
for (const num of nums) {
if (num % 2 === 0) {
return num; // 找到第一个偶数直接返回
}
}
return null;
}
5.4 遍历中删除元素:这是个大坑
在遍历数组时删除元素是最容易出 bug 的场景之一。用 forEach 遍历时删除当前元素会导致索引错位,for-of 同样有问题:
javascript复制const arr = [1, 2, 3, 4, 5];
for (const num of arr) {
if (num === 3) {
arr.splice(arr.indexOf(num), 1); // 删除 3
}
console.log(num);
}
// 输出:1 2 3 5,4 没了
删除了 3 之后,数组前移,4 的索引变成了原来 3 的位置,迭代器继续往后走的时候直接跳过了 4。这不是 for-of 的 bug,而是所有基于索引的遍历共有的问题。解决思路有两个:
- 如果必须先删除再遍历,可以反向遍历,或者收集要删除的元素,遍历结束后统一删;
- 如果筛选逻辑简单,直接用
filter生成新数组,别在循环里做删除操作。
javascript复制// 推荐:收集后统一删除
const toRemove = [];
for (const num of arr) {
if (num === 3) toRemove.push(num);
}
arr.splice(0, arr.length, ...arr.filter(item => !toRemove.includes(item)));
5.5 Generator 与 for-of:手动暂停和恢复的遍历
for-of 和 Generator 的配合是一对黄金搭档。Generator 函数返回的对象本身就是一个可迭代对象,遍历它会自动执行到每个 yield 处暂停。
javascript复制function* numberGenerator() {
yield 1;
yield 2;
yield 3;
}
for (const num of numberGenerator()) {
console.log(num); // 输出 1 2 3
}
更有意思的是,for-of 遇到底层迭代器提前结束时,会自动调用迭代器的 return() 方法(如果存在),这给了 Generator 做清理工作的机会。比如生成无限序列时,你可以在循环里用 break 跳出,生成器内部通过 finally 块感知退出:
javascript复制function* infiniteCounter() {
let i = 0;
try {
while (true) {
yield i++;
}
} finally {
console.log('循环被跳出了,做清理工作');
}
}
for (const num of infiniteCounter()) {
console.log(num);
if (num >= 2) break;
}
// 输出 0 1 2,并打印清理日志
6. 实际业务场景:for-of 在 Vue/React 项目里的典型应用
6.1 遍历对象数组并做条件过滤
日常业务里,从后端拿到的数据多半是对象数组,用 for-of 配合 if 条件做筛选特别顺手:
javascript复制const orders = [
{ id: 1, status: 'pending', amount: 100 },
{ id: 2, status: 'paid', amount: 250 },
{ id: 3, status: 'pending', amount: 80 }
];
const pendingOrders = [];
for (const order of orders) {
if (order.status === 'pending') {
pendingOrders.push(order);
}
}
当然纯函数式的写法用 filter 更地道,但当你需要在遍历过程中同时做多个判断、聚合计算、或者提前终止时,for-of 的优势就体现出来了。
6.2 遍历 Map 做表单联动
在 Vue 或 React 项目里,用 Map 管理表单字段状态,然后通过 for-of 生成校验结果,是很常见的模式:
javascript复制const fieldState = new Map([
['username', { value: 'zhangsan', valid: true }],
['email', { value: 'not-an-email', valid: false }]
]);
const errors = [];
for (const [field, state] of fieldState) {
if (!state.valid) {
errors.push(`${field} 格式不正确`);
}
}
6.3 异步序列遍历
for-of 配合 async/await 还有一个特殊用法:按顺序执行异步操作。forEach 里写 await 是无效的,因为 forEach 不会等待回调返回的 Promise。for-of 则天然支持逐次等待:
javascript复制const urls = ['/api/a', '/api/b', '/api/c'];
async function fetchAll() {
const results = [];
for (const url of urls) {
const response = await fetch(url); // 这一个完成才进入下一个
results.push(await response.json());
}
return results;
}
这个模式在做批量接口请求、逐条处理上传任务、或者需要保持顺序的场景下非常实用。注意这里用的是 for-of,不是 for-each,两者在异步遍历上的差异是一个高频踩坑点。
6.4 遍历生成器数据流
后台管理系统里经常需要处理分页数据。如果后端接口是游标分页,用生成器配合 for-of 可以让代码非常干净:
javascript复制function* pageFetcher(api, firstPageParams) {
let params = firstPageParams;
while (true) {
const result = await api(params);
yield result.list;
if (!result.hasNext) return;
params = { ...params, cursor: result.nextCursor };
}
}
// 使用
for await (const list of pageFetcher(api, { pageSize: 20 })) {
for (const item of list) {
// 处理每一页里的每条数据
}
// 如果只需要前 3 页,直接 break
}
注意这里用了 for await...of,它是专门处理异步迭代器的语法,和普通 for-of 是两回事,但背后的理念一脉相承。如果你的接口是异步生成器,用 for await...of 就能按顺序消费所有分页数据。
7. 常见报错与排查技巧:循环相关错误实录
7.1 object is not iterable 的排查思路
TypeError: object is not iterable 是最常见的 for-of 报错。原因基本就是你对一个普通对象使用 for-of:
javascript复制const obj = { name: '张三' };
for (const item of obj) { // 报错:obj is not iterable
console.log(item);
}
排查步骤很简单:先确认遍历的目标是不是内置可迭代对象(数组、字符串、Set、Map、arguments、NodeList);如果是普通对象,想遍历键值对就改用 Object.entries(obj) 转成数组,或者用 for-in(但注意原型链问题)。
7.2 在 Vue 模板里用了 for-of
在 Vue 模板的 v-for 里,不能直接写 for-of 的语法,要用 v-for="(value, index) in array" 这种 Vue 自定义的遍历语法。如果非要在模板表达式里对 Map 或 Set 遍历,最好的做法是在计算属性或方法里先转成数组,再交给 v-for 渲染。这个问题在 Vue 3 里同样存在,别把原生 JS 语法和模板语法弄混。
7.3 for-of 和 for-in 用混了变量类型
有人习惯性写了 for (const index of arr),结果发现 index 拿到的是值而不是索引,然后代码逻辑全部错乱。这类问题通过名字区分能避免:用 for-of 时变量名用 item、value、element;用 for-in 时用 key、index、prop。虽然只是命名习惯,但能在读代码时大幅降低认知负担。
7.4 forEach 中异步失效带来的假“问题”
很多人在 forEach 里写 await,发现结果不对,然后改成 for-of 发现问题解决了。背后的原因前面说过:forEach 不等待 Promise。如果你在写批量处理代码时发现结果顺序不对或根本没有按预期执行,先检查一下是不是把 for-of 写成了 forEach。
7.5 循环里 indexOf 开销过大的性能问题
在 for-of 循环体内用 indexOf 去查找元素位置,每次都是 O(n) 操作,整个循环就是 O(n²)。如果数据量上万,运行时间会指数级暴涨。替代方案是用 Map 或 Set 建立索引:
javascript复制// 不推荐
for (const item of list) {
const idx = list.indexOf(item); // 每次 O(n)
}
// 推荐
const indexMap = new Map();
list.forEach((item, i) => indexMap.set(item.id, i));
for (const item of list) {
const idx = indexMap.get(item.id); // O(1)
}
8. 个人经验总结与最终建议
写了这么多年 JS,我自己的遍历选型原则是这样:数组遍历默认用 for-of,需要索引就用 entries() 配合解构;代码风格偏函数式、不需要提前退出的时候用 forEach;遍历普通对象的键值对用 Object.entries + for-of;对性能和内存极致的场景(百万级数组、游戏引擎、Canvas 渲染循环)才退回传统 for。
for-of 最大的价值,不是它比别的遍历方式快多少,而是它统一了 JavaScript 各种数据结构的遍历体验。从数组到字符串,从 Set 到 Map,从 DOM 集合到 Generator,你只需要记住一种语法就能遍历所有可迭代对象。这个“统一”的价值,在代码维护和团队协作里体现得特别明显。
最后分享一个小技巧:在任何面试或技术分享场合,如果能主动从 for-of 聊到背后的迭代器协议,再补一个“如何让一个普通对象支持 for-of”的现场实现,面试官基本上会认为你对 ES6 的理解是扎实的。这个知识点覆盖面大、可深可浅,是 JavaScript 基础面试里性价比极高的一个锚点。
