- 教程
- 文档
【免费下载链接】30-seconds-of-code
Coding articles to level up your development skills
遍历数组、对象、字符串乃至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
相关推荐
30 seconds of code:JavaScript 数组比较的完整实战指南
30 seconds of code:JavaScript 数组比较的完整实战指南 导读 在 JavaScript 中比较两个数组是否"相等",是开发中最高频也
教程文档30-seconds-of-code:重构 for...in 循环以消除 ESLint 警告的三种方案
30 seconds of code:重构 for...in 循环以消除 ESLint 警告的三种方案 ESLint 是 JavaScript 开发中最常用的静
教程文档30 seconds of code:JavaScript 异步数组循环的三种方案与避坑指南
30 seconds of code:JavaScript 异步数组循环的三种方案与避坑指南 异步操作与数组循环的组合是 JavaScript 开发中最容易踩坑
教程文档
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考