两种循环,两种不同的用途
JavaScript 有两种看起来几乎一模一样、作用却截然不同的循环。for...of 遍历可迭代对象的值,for...in 遍历对象的键。只差一个单词,行为却天差地别。
一个例子就能说清全部:
第一个循环打印 apple、banana、cherry,也就是值。第二个打印 0、1、2,也就是键,而且是字符串。选错了的话,你可能要花十分钟琢磨为什么数组里全是数字。
for...of:从任何可迭代对象中取值
for...of 是你最常用的循环。它适用于 JavaScript 认为可迭代的任何东西:数组、字符串、Map、Set、NodeList、生成器等等。
不用摆弄索引,也不用写 scores[i]。你要每个值,就得到每个值。字符串也是可迭代的,for...of 会逐个字符地遍历它:
这对大多数 Unicode 码点也能正确处理,比起用数字下标访问字符串,这是一个虽小却实在的优势。
用 for...of 获取索引
for...of 唯一不会直接给你的就是位置。需要它时,把循环和 entries() 搭配使用:
names.entries() 会产出 [index, value] 对,左边的解构把它们拆成两个变量。这通常比仅仅为了得到 i 就退回到经典的 for (let i = 0; ...) 更简洁。
for...in:对象的键
for...in 是为普通对象设计的。它遍历可枚举的字符串键:
注意你得到的是键,而不是值。要读取值,需要用 user[key] 回到对象中取。每个键都是字符串,即使它看起来像数字。
for...in 还会沿着原型链向上查找,所以可能会遍历到继承来的属性。对于你自己写的字面量对象,这很少有影响;但在遍历类的实例或来自库的对象时,最好多加防范:
Object.hasOwn(user, key) 会跳过所有继承的属性。在现代代码中,大多数人完全不用 for...in,而是改用 Object.keys、Object.values 或 Object.entries,这正是下一节的内容。
遍历对象的现代写法
不用 for...in,而是把 for...of 和某个 Object.* 辅助方法结合起来。三个方法中选一个符合需要的即可:
Object.entries 尤其好用:把 [key, value] 对解构出来,读起来就像普通的句子。而且这些方法只返回对象自身的可枚举属性,你不用担心继承来的杂七杂八。
不要对数组使用 for...in
技术上是合法的,但很容易出问题:
你会得到 0、1、2,还有 tag。任何人挂到这个数组上的属性,或者通过 polyfill 挂到 Array.prototype 上的属性,都会出现。键是字符串,所以 key + 1 会变成字符串拼接而不是加法。而且在某些边界情况下,遍历顺序不能保证和数组顺序一致。
经验法则:
- 数组的值?
for...of arr。 - 数组的索引和值?
for...of arr.entries()。 - 只要数组索引、按数计数?经典的
for (let i = 0; i < arr.length; i++)。 - 对象的键或键值对?
Object.keys(obj)/Object.entries(obj)配合for...of。
总之:for...in 是个小众工具。for...of 加上 Object 辅助方法就能满足你的所有需要。
break 和 continue 在两者中都能用
两种循环都支持常用的提前退出手段:
continue 跳到下一次迭代;break 完全退出循环。这也是优先选择 for...of 而不是 .forEach() 的主要原因:forEach 的回调无法用 break 跳出循环,而 for...of 可以。
对比总结
四种循环,四种用途,一个思维模型:想从可迭代对象中取值,就用 for...of。想处理对象的属性,就通过 Object.keys / Object.values / Object.entries,同样配合 for...of。只有在极少数确实需要对象的每个可枚举字符串键(无论是否继承)的情况下,才用 for...in。
下一步:真值和假值
JavaScript 中的每个循环和 if 最终都会问同一个问题:这个值算不算真?答案并不总是显而易见:空字符串、零、null 和 undefined 的表现都可能出乎你的意料。下一页讲真值和假值。
常见问题
JavaScript 中 for...of 和 for...in 有什么区别?
for...of 遍历可迭代对象(如数组、字符串、Map 或 Set)的值。for...in 遍历对象的键(属性名)。对于数组 ['a', 'b'],for...of 得到的是 'a' 和 'b';for...in 得到的是字符串形式的 '0' 和 '1'。
可以对对象使用 for...of 吗?
不能直接用,普通对象不是可迭代的。先用 Object.keys(obj)、Object.values(obj) 或 Object.entries(obj) 得到一个数组,再用 for...of 遍历。常见写法是 for (const [key, value] of Object.entries(obj))。
为什么不应该对数组使用 for...in?
技术上可行,但 for...in 会遍历每个可枚举属性,包括继承来的属性,以及别人添加到 Array.prototype 上的任何额外属性。它返回的键也是字符串,而且对于类似整数的键,并不是在所有情况下都能完全保证顺序。要取值就用 for...of,需要索引时用经典的 for 循环。