for...in、for...of、forEach 和 map 有什么区别?forEach 能用 break 跳出吗?
for...in 遍历对象的可枚举字符串键,会带上原型链上的键;for...of 按迭代器取值,能遍历数组、字符串、Map、Set;forEach 对每个元素调用回调,没有返回值;map 返回由回调结果组成的新数组。forEach 不能用 break,只有抛出异常才能中途停下,要提前结束就改用 for...of、some 或 find。
简答
拿一个数组
['a', 'b']来说,for...in拿到的是键'0'、'1',而且是字符串;for...of拿到的是值'a'、'b';forEach和map是数组方法,对每个元素调用一次回调,区别是forEach返回undefined,map返回由回调返回值组成的新数组。它们的来源不同。
for...in走的是属性枚举,会遍历对象自己和原型链上可枚举的字符串键,所以适合普通对象,不适合数组。for...of走的是迭代器,数组、字符串、Map、Set、NodeList都能用,普通对象不行。选的时候看要做什么:遍历对象的键用
Object.keys或for...in;要转换成新数组用map;只是挨个做点事用forEach或for...of;要中途停下或者要await,用for...of。
forEach不能用break,写在回调里是语法错误,return也只是结束这一次回调。MDN 写明,除了抛异常,没有办法停止forEach。要提前结束,就改用for...of加break,或者用some、find这类找到就停的方法。
同一个数组,四种写法拿到什么
下面给数组加了一个非下标属性 extra,再在 Array.prototype 上加一个可枚举的方法,看看每种写法会碰到哪些东西。
const list = ['a', 'b'];
list.extra = 'x';
Array.prototype.hello = function () {};
const inKeys = [];
for (const key in list) inKeys.push(key);
const ofValues = [];
for (const value of list) ofValues.push(value);
const eachResult = list.forEach((value) => value.toUpperCase());
const mapResult = list.map((value) => value.toUpperCase());
console.log(inKeys.join(',')); // 0,1,extra,hello
console.log(typeof inKeys[0]); // string
console.log(ofValues.join(',')); // a,b
console.log(String(eachResult)); // undefined
console.log(mapResult.join(',')); // A,B
delete Array.prototype.hello;
for...in 拿到了四个键,除了下标,还有 extra 和原型上的 hello。因为它遍历的是可枚举属性,下标只是名字恰好是整数的属性,和 extra 没有区别,原型链上可枚举的属性也会被带上。下标拿到的是字符串 '0',拿去做加法会变成字符串拼接。所以 MDN 建议遍历数组用普通 for 循环、forEach 或 for...of。
for...of 只拿到 a、b。它不看属性,而是调用数组的迭代器,数组的迭代器只按下标产出元素。
forEach 把回调的返回值丢掉,自己返回 undefined。map 收集每次的返回值,组成新数组。只为了副作用而调用 map,会白白造一个数组,这种时候用 forEach 或 for...of。
为什么 forEach 停不下来
break 和 continue 只能写在循环语句里。forEach 的回调是一个普通函数,不是循环体,所以在回调里写 break 会直接报语法错误。在回调里 return,只是让这一次回调结束,forEach 会接着调用下一个元素。
MDN 的说法是:除了抛出异常,没有办法停止或跳出 forEach,需要这种行为时,forEach 就不是合适的工具。用 try/catch 包住再 throw 虽然能停,但读代码的人很难看出意图。下面是更直接的写法:
const numbers = [3, 8, 12, 5];
let firstBig;
for (const n of numbers) {
if (n > 10) {
firstBig = n;
break;
}
}
const checked = [];
const hasBig = numbers.some((n) => {
checked.push(n);
return n > 10;
});
console.log(firstBig); // 12
console.log(String(hasBig)); // true
console.log(checked.join(',')); // 3,8,12
for...of 遇到 12 就 break 出去了。some 也一样,回调返回 true 以后就不再往后调用,从 checked 能看到最后一个 5 没有被检查。只想拿到第一个满足条件的元素,用 find 更短。
还有哪些差别
forEach 和 map 会跳过稀疏数组里的空位,for...of 会把空位读成 undefined,for...in 也会跳过空位。稀疏数组的问题见《数组常用方法怎样选择,哪些会修改原数组?》。
forEach 不会等待 async 回调,MDN 写明它期望的是同步函数。要一个一个等,用 for...of 加 await;要一起跑再等全部完成,用 map 收集 Promise 再交给 Promise.all。
for...of 中途 break 或抛出异常时,会调用迭代器的 return() 方法做清理,见《JavaScript 迭代器关闭和 return 方法有什么作用?》。for...in 的键顺序和各个取键方法的范围,见《Object.keys、for...in 和 Reflect.ownKeys 会返回哪些属性?》。
常见追问
for...of 能遍历普通对象吗?
不能,普通对象没有实现迭代器,会抛出 TypeError。要遍历键值对,写
for (const [key, value] of Object.entries(obj))。
forEach 里用 return 有什么效果?
只结束当前这一次回调,相当于普通循环里的
continue。后面的元素还会继续被调用,forEach本身的返回值始终是undefined。