☰
30 seconds of code:JavaScript 中 for...in、for...of 与 forEach() 三种迭代方式的完整对比
2026/10/4 9:49:26 网站建设 项目流程
  • 教程
  • 文档

【免费下载链接】30-seconds-of-code

Coding articles to level up your development skills

项目地址:https://gitcode.com/gh_mirrors/30/30-seconds-of-code
点击查看免费下载

遍历数组、对象、字符串乃至Map、Set,是 JavaScript 开发中最高频的操作之一,但for...in、for...of与Array.prototype.forEach()三者到底有何区别,却常常让初学者甚至资深开发者混淆。本篇基于 30 seconds of code 仓库中的核心文档 for-in-for-of-foreach,系统梳理三种迭代方式的适用对象、取值语义、边界陷阱,并结合作者仓库内 array-map-vs-foreach、for-each-right、for-loop-early-break、async-array-loops 等关联片段,给出每个场景下最正确的选型结论。读完本文,你将能一眼判断任意数据结构该用哪种迭代方式,并避开原型链污染、TypeError: not iterable、回调无法提前退出等常见坑。

for...in:遍历可枚举属性(含继承属性)

for...in用于遍历一个对象的所有可枚举属性,包括继承自原型链的可枚举属性。它适用于数组、字符串和普通对象,但不能用于Map或Set。

看文档中的四个例子:

for (let prop in ['a', 'b', 'c']) console.log(prop); // 0, 1, 2 (数组索引) for (let prop in 'str') console.log(prop); // 0, 1, 2 (字符串索引) for (let prop in {a: 1, b: 2, c: 3}) console.log(prop); // a, b, c (对象属性名) for (let prop in new Set(['a', 'b', 'a', 'd'])) console.log(prop); // undefined (没有可枚举属性)

关键点在于for...in取出的是键(key)而不是值:数组上取出的是索引下标,字符串上取出的是字符下标,普通对象上取出的是属性名。而对Set来说,它内部并没有暴露可枚举属性,因此循环体内什么都拿不到。

警惕:原型链上的可枚举属性

for...in会沿原型链向上遍历所有可枚举属性,这通常是我们并不想要的行为。仓库中的另一篇文章 eslint-refactor-for-in 开篇就引出了 ESLint 对它的经典警告:

for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.eslint(no-restricted-syntax)

如果你确实想只遍历对象自身的键,需要配合Object.prototype.hasOwnProperty()做过滤;而更推荐的做法是直接用Object.keys()、Object.values()、Object.entries()把键/值/键值对先取成数组,再交给数组方法处理。该文给出的三个替代方案如下:

const data = [3, 4]; // 与 for (let k in data) console.log(k) 行为完全一致 Object.keys(data).forEach(k => console.log(k)); // 0 1 // 只想拿值时,用 Object.values() Object.values(data).forEach(v => console.log(v)); // 3 4 // 键和值都要时,用 Object.entries() Object.entries(data).forEach(e => console.log(e[0], e[1])); // 0 3, 1 4

这三个静态方法都只返回对象自身的键/值/键值对,天然绕开了原型链问题,是重构for...in最稳妥的手段。

for...of:遍历可迭代对象的值

for...of用于遍历可迭代(iterable)对象,取出的是元素的值而不是属性。它适用于数组、字符串、Map、Set,但不能用于普通对象——因为普通对象默认没有实现迭代器协议。

for (let val of ['a', 'b', 'c']) console.log(val); // a, b, c (数组元素值) for (let val of 'str') console.log(val); // s, t, r (字符串字符) for (let val of {a: 1, b: 2, c: 3}) console.log(val); // TypeError: obj is not iterable for (let val of new Set(['a', 'b', 'a', 'd'])) console.log(val); // a, b, d (Set 去重后的值)

注意上面普通对象的例子会直接抛出TypeError: not iterable,因为for...of依赖目标对象的Symbol.iterator方法,而普通对象并没有实现它。同时这个例子也顺带展示了Set的一个特性:元素自动去重,['a', 'b', 'a', 'd']迭代出来只有a, b, d。

想要索引?用Array.prototype.entries()

for...of默认只能拿到值,这常被拿来与forEach()的「值 + 索引」能力做对比。但仓库中的 index-for-of-loop 指出,这其实不是限制——结合Array.prototype.entries()即可同时拿到索引和值:

const items = ['a', 'b', 'c']; for (let [index, item] of items.entries()) { console.log(`${index}: ${item}`); } // LOGS: 0: a, 1: b, 2: c

字符串可以先用展开运算符(...)转成数组再如法炮制;Map.prototype.entries()和Set.prototype.entries()也提供了完全相同的用法。

与 async/await 的天然亲和力

for...of与for循环在异步场景下还有一个显著优势:循环体内可以使用await,且所有 Promise 会按顺序依次执行。仓库中的 async-array-loops 给出了对照:

const asyncUppercase = item => new Promise(resolve => setTimeout(() => resolve(item.toUpperCase()), Math.floor(Math.random() * 1000)) ); const uppercaseItems = async () => { const items = ['a', 'b', 'c']; for (item of items) { const uppercaseItem = await asyncUppercase(item); console.log(uppercaseItem); } console.log('Items processed'); }; // Logs: 'A', 'B', 'C', 'Items processed' (严格按顺序)

如果你需要并行执行,则改用Promise.all()+Array.prototype.map();但千万不要把async回调直接传给forEach(),因为它不会等待 Promise 完成,代码会继续往下跑,导致「Items processed」先于异步结果输出。

Array.prototype.forEach():数组元素的专用方法

forEach()是Array原型上的方法,只能作用于数组(以及类数组对象),但它在遍历时可以同时访问元素的值和索引:

['a', 'b', 'c'].forEach( val => console.log(val) // a, b, c (数组元素值) ); ['a', 'b', 'c'].forEach( (val, i) => console.log(i) // 0, 1, 2 (数组索引) );

回调函数还接收可选的第三个参数——数组本身,这在需要内部引用原数组(如合并相邻元素)时很有用。

返回 undefined:与 map() 的本质区别

forEach()的返回值恒为undefined,它只负责「对每个元素执行副作用」,不产生新数组。这与Array.prototype.map()形成鲜明对照——map()返回一个由回调结果构成的新数组,代表「转换」。仓库中的 array-map-vs-foreach 用一个例子把二者区分得清清楚楚:

const numbers = [1, 2, 3, 4, 5]; numbers.forEach(num => console.log(num * 2)); // 无返回值,仅输出: 2, 4, 6, 8, 10 const doubledNumbers = numbers.map(num => num * 2); // 返回新数组: [2, 4, 6, 8, 10]

一句话记忆法:map()是转换(transformation),forEach()是遍历(iteration)。当你只是想对每个元素执行副作用(打印、写入、发请求)时用forEach();当你想基于原数组生成一个新数组时,务必用map()。

两个不能忽略的短板:不能 break、不能 await

forEach()的第一个短板是无法提前跳出循环——break、continue、return都无法终止它。仓库中的 for-loop-early-break 展示了一个典型场景:判断两个数组是否相等,用forEach()即使已经发现不相等,也会把largeArray全部遍历完:

const smallArray = [0, 2]; const largeArray = Array.from({ length: 1000 }, (_, i) => i); const areEqual = (a, b) => { let result = true; a.forEach((x, i) => { if (!result) return; if (b[i] === undefined || x !== b[i]) result = false; }); return result; }; areEqual(largeArray, smallArray); // false // 但会白白遍历 largeArray 的全部 1000 项

而换成传统for循环加提前return,一遇到第一个不匹配项就立刻退出:

const areEqual = (a, b) => { for (let i in a) { if (b[i] === undefined || a[i] !== b[i]) return false; } return true; }; areEqual(largeArray, smallArray); // false // 只会遍历到第一个不匹配的位置就返回

第二个短板在前文已提:async回调传入forEach()后,外层await并不会等待所有异步操作完成,输出顺序与预期完全不可控(见 async-array-loops 中的第三种写法)。

反向遍历:slice + reverse + forEach

forEach()默认从左到右遍历。若想从右向左,仓库中的 for-each-right 给出了一个优雅且不污染原数组的组合拳——先slice()浅拷贝,再reverse(),最后forEach():

const forEachRight = (arr, callback) => arr.slice().reverse().forEach(callback); forEachRight([1, 2, 3, 4], val => console.log(val)); // '4', '3', '2', '1'

三分钟选型速查表

迭代方式取出的是什么适用对象不适用对象能否提前退出是否支持 await 顺序执行
for...in键(含继承的可枚举属性)数组、字符串、普通对象Map、Set可以(break/return)可以
for...of值数组、字符串、Map、Set普通对象(非 iterable)可以(break/return)可以
Array.prototype.forEach()值 + 索引 + 数组本身数组普通对象、Map、Set不可以不可以(异步会乱序/不等)

补充两条实战经验:

  • 遍历普通对象的键/值:优先Object.keys()/Object.values()/Object.entries()+ 数组方法,彻底避开for...in的原型链遍历问题(参考 eslint-refactor-for-in);
  • 需要索引的for...of:用items.entries()解构[index, item],兼顾可读性与break能力(参考 index-for-of-loop);
  • 需要提前退出或按序 await 的场景:直接用for/for...of,这是数组方法给不了的能力(参考 for-loop-early-break 与 async-array-loops)。

相关阅读

  • Array.prototype.map() 与 Array.prototype.forEach() 的区别:返回值决定两者不可互换
  • 在 for...of 循环中获取数组索引:entries()的妙用
  • 从右向左遍历数组:slice().reverse().forEach()组合
  • 需要提前退出时使用 for 循环:forEach()无法 break 的解决方案
  • JavaScript 异步数组循环:for...of顺序执行 vsPromise.all()并行执行
  • 重构 for...in 以规避 ESLint 警告:Object.keys/values/entries替代方案
  • 上述片段统一收录于 JavaScript Array Methods 与 JavaScript Interview Questions 两个集合中,前者汇总了数组遍历与变换的常用技巧,后者则把forEach与map的对比列为高频面试题之一
  • 教程
  • 文档

【免费下载链接】30-seconds-of-code

Coding articles to level up your development skills

项目地址:https://gitcode.com/gh_mirrors/30/30-seconds-of-code
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询