最近整理 JS 循环相关内容时,我发现这些问题经常会被放在一起问:for...of 和 for...in 怎么选,数组用 map 还是 forEach,对象怎么遍历,循环里 continue、break、return 又分别会停到哪里。
这些知识点单独看都不难,但如果项目里混着用,很容易写出语义不清楚的代码。我的理解是,先分清楚“你要遍历的是值、索引、键,还是要生成新数组”,大部分选择就清楚了。
一、for…of 和 for…in 的区别
for...of 遍历的是可迭代对象里的“值”。数组、字符串、Map、Set、NodeList 这类对象都可以用。
const arr = ['a', 'b', 'c'];
for (const item of arr) {
console.log(item);
}
// a b c
如果是字符串,拿到的也是每一个字符:
for (const char of 'abc') {
console.log(char);
}
// a b c
for...in 遍历的是对象的可枚举属性名。对普通对象来说,拿到的是 key;对数组来说,拿到的是索引。
const obj = { name: 'Tess', age: 18 };
for (const key in obj) {
console.log(key, obj[key]);
}
// name Tess
// age 18
所以我一般会这样记:
- 遍历数组、字符串、Set、Map 的值:优先
for...of。 - 遍历对象 key:可以用
for...in,但要注意原型链。 - 不要用
for...in遍历数组值,因为它拿到的是索引,而且还可能遍历到额外的可枚举属性。
普通对象默认不是 iterable,所以直接对对象用 for...of 会报错:
const obj = { a: 1, b: 2 };
for (const item of obj) {
console.log(item);
}
// TypeError: obj is not iterable
二、对象怎么遍历
对象遍历我更常用 Object.keys、Object.values、Object.entries,因为它们只处理对象自身的可枚举属性,语义也更明确。
只需要 key:
const obj = { a: 1, b: 2, c: 3 };
Object.keys(obj).forEach((key) => {
console.log(key, obj[key]);
});
只需要 value:
Object.values(obj).forEach((value) => {
console.log(value);
});
key 和 value 都需要:
Object.entries(obj).forEach(([key, value]) => {
console.log(key, value);
});
for...in 也能遍历对象,但它会沿着原型链找可枚举属性,所以严谨一点要加判断:
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
console.log(key, obj[key]);
}
}
实际项目里,如果只是处理普通接口返回的数据,我会优先用 Object.entries,因为同时拿 key 和 value 最方便。
三、map 和 forEach 的区别
forEach 是“遍历一下”,它会对每个元素执行回调,但返回值是 undefined。
const numbers = [1, 2, 3];
const result = numbers.forEach((num) => {
console.log(num);
});
console.log(result); // undefined
map 是“把一个数组映射成另一个新数组”,它会收集每次回调的返回值。
const numbers = [1, 2, 3];
const squares = numbers.map((num) => {
return num * num;
});
console.log(squares); // [1, 4, 9]
所以选择标准很简单:
- 只是执行副作用,比如打印、提交、修改外部状态:用
forEach。 - 需要根据原数组生成一个新数组:用
map。
React 里渲染列表通常用 map,就是因为它需要把数据数组转换成 JSX 数组。
items.map((item) => (
<ListItem key={item.id} data={item} />
));
四、map 里有没有 return 的区别
map 最容易踩的坑是写了大括号,但忘了 return。
const numbers = [1, 2, 3];
const result = numbers.map((num) => {
num * num;
});
console.log(result); // [undefined, undefined, undefined]
因为 map 会把每次回调的返回值放进新数组。这里回调没有返回任何东西,所以每一项都是 undefined。
正确写法可以显式 return:
const result = numbers.map((num) => {
return num * num;
});
也可以用箭头函数的隐式返回:
const result = numbers.map((num) => num * num);
React 里也一样。如果用了小括号,是隐式返回;如果用了大括号,就要自己写 return。
// 可以
items.map((item) => (
<ListItem key={item.id} data={item} />
));
// 也可以
items.map((item) => {
return <ListItem key={item.id} data={item} />;
});
// 容易出错
items.map((item) => {
<ListItem key={item.id} data={item} />;
});
五、continue、break、return 停在哪里
循环里这三个关键字的范围不一样。
continue 是跳过当前这一轮,继续下一轮。
for (const num of [1, 2, 3, 4]) {
if (num % 2 === 0) {
continue;
}
console.log(num);
}
// 1 3
break 是结束整个循环,但循环外面的代码还会继续执行。
for (const num of [1, 2, 3, 4]) {
if (num === 3) {
break;
}
console.log(num);
}
console.log('end');
// 1 2 end
return 是直接结束当前函数。只要执行到 return,函数里后面的代码都不会继续执行。
function findFirstEven(list) {
for (const num of list) {
if (num % 2 === 0) {
return num;
}
}
return null;
}
findFirstEven([1, 3, 4, 6]); // 4
我会这样区分:想跳过当前数据,用 continue;找到目标后不想继续循环,用 break;整个函数已经有结果了,用 return。
六、最后
JS 里的循环不是只有一种写法。数组、对象、Map、Set、字符串这些数据结构不同,适合的遍历方式也不同。我的选择标准一般是先问自己:我要的是值、key、key-value,还是一个新数组?
如果只是遍历数组值,for...of 很直接;如果是普通对象,Object.entries 通常最顺手;如果要生成新数组,用 map;如果只是做副作用,用 forEach。至于 continue、break、return,重点是看它们到底影响当前一轮、整个循环,还是整个函数。