js中的循环

2022/09/18

最近整理 JS 循环相关内容时,我发现这些问题经常会被放在一起问:for...offor...in 怎么选,数组用 map 还是 forEach,对象怎么遍历,循环里 continuebreakreturn 又分别会停到哪里。

这些知识点单独看都不难,但如果项目里混着用,很容易写出语义不清楚的代码。我的理解是,先分清楚“你要遍历的是值、索引、键,还是要生成新数组”,大部分选择就清楚了。

一、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

所以我一般会这样记:

  1. 遍历数组、字符串、Set、Map 的值:优先 for...of
  2. 遍历对象 key:可以用 for...in,但要注意原型链。
  3. 不要用 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.keysObject.valuesObject.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]

所以选择标准很简单:

  1. 只是执行副作用,比如打印、提交、修改外部状态:用 forEach
  2. 需要根据原数组生成一个新数组:用 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。至于 continuebreakreturn,重点是看它们到底影响当前一轮、整个循环,还是整个函数。