JavaScript各遍历方法对比

前端4 min
#JavaScript
目录(5)

各遍历方法简要对比

支持的数据类型break、continuereturnthrowasync、await特点
forIArray支持支持支持支持最基础的循环,语法繁琐。三个条件,均可省略。
forInArray,Object支持支持支持支持主要用于对象的遍历,获取其key键,但获取的key键均为String类型。不建议对数组使用,特定情况下循环会出错。
forOfArray,Set,Map,String,引用类型等(更多类型可以通过iterator定义)支持支持支持支持for … of 修复了for … in 对数组操作的若干问题。其主要遍历对象的值,但在对数组遍历时可以借助ES6数组提供的方法实现key、value的遍历。for … of 可以通过实现Iterator接口对任意数据类型进行遍历。
forEachArray,Set,Map,引用类型不支持可以使用,仅能跳出当前循环,与continue效果相似。支持不支持在循环中break、continue俩个关键字都无效,但是可以通过return结束当轮循环,通过throw退出循环体。forEach 不会直接改变调用它的对象,但是那个对象可能会被 callback 函数改变。在使用promise 或 async 作为callback函数时会出现执行顺序的问题。

for … in

该方法常用于取对象的key键,但在对数组使用时会暴露出几个缺点。

对数组操作的若干问题

数组的key键都是Number,但for … in 取出的均为String

const arr = [ 5, 4, 3, 2, 1 ];

for (const i in arr) {
  console.log(i); // '0', '1', '2', '3', '4'
}

循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。 导致遍历的次数与arr.length得到的长度两者不符。

const arr = [ 5, 4, 3, 2, 1 ];

arr.name = '男';
for (const i in arr) {
  console.log(i); // '0', '1', '2', '3', '4', 'name'
}                 // 遍历6次
console.log(arr.length); // 5

for … of

该方法可以遍历大多常用数据类型,其中Object迭代时需要进行处理,一些复杂类型也可以通过自定义钩子进行定制迭代方式。同时其在for … in 的基础上完善了对数组的操作。

修复for … in 操作数组的缺点

let arr = [3, 5, 7];
arr.foo = 'hello';

for (let i in arr) {
  console.log(i); // "0", "1", "2", "foo"
}

for (let i of arr) {
  console.log(i); //  "3", "5", "7"
}

遍历中可修改原数组的值

定义循环变量时用const不可修改原对象,通过let定义即可。

let iterable = [10, 20, 30];

for (const value of iterable) { 
  console.log(value); // 10 ,20 ,30 
}

for (let value of iterable) {
    value += 1;
    console.log(value); // 11 ,21 ,31 
}

在遍历数组时获取数组的key键和value值

可以借助ES6数组示例的entries(),keys()和values()方法,实现遍历数组的key和value。

const iterable = [ 10, 20, 30, 'A', 'B', 'C' ];

for (const index of iterable.keys()) {
  console.log(index); // 0, 1, 2, 3, 4, 5
}

for (const elem of iterable.values()) {
  console.log(elem); // 10, 20, 30, 'A', 'B', 'C'
}


for (const [ index, elem ] of iterable.entries()) {
  console.log(index, elem);// 0 10, 1 20, 2 30, 3 'A', 4 'B', 5 'C'
}

Object对象的遍历和Iterator的实现

该方法默认无法遍历Object对象,因为其不确定遍历属性应该采用哪种顺序,故而需要进行自定义。

这块笔者理解不透彻,感兴趣的小伙伴可以移步该连接:Iterator和for … of


forEach

该方法适用于Array、Set、Map、引用类型的遍历,但不支持break,continue和return方法,以及该改变数组值等操作。

break、continue、returen、throw 使用示例

在循环中无法通过break,continue结束当前循环,但是可以通过return结束当轮循环,通过throw退出循环体。

const arr = [ 1, 2, 3, 4, 5 ];

arr.forEach(function(item) {
  if (item === 3) {
    break; // Illegal break statement
  }
  console.log(item);
});

arr.forEach(function(item) {
  if (item === 3) {
    continue;; // Illegal continue statement: no surrounding iteration statement
  }
  console.log(item);
});

arr.forEach(function(item) {
  if (item === 3) {
    return;
  }
  console.log(item); // 1 , 2 , 4 , 5
});

try {
  arr.forEach(function(item) {
    if (item === 3) {
      throw new Error('EndIterative');
    }
    console.log(item);  // 1 , 2 
  });
} catch (error) {
  if (error.message !== 'EndIterative') throw error;
}

修改迭代对象的方法

forEach被调用时,不会改变原数组,也就是调用它的数组(尽管 callback 函数在被调用时可能会改变原数组)。即,forEach 不会直接改变调用它的对象,但是那个对象可能会被 callback 函数改变。

实际调用过程中发现,forEach迭代是对原对象进行复制,在其副本上进行操作。 如果是基本数据类型在callback不能对其进行改变,如果是引用类型,只能改变部分属性。 但不论那种数据类型,都可以通过forEach的第三个参数对原对象进行修改。

对基本类型的数组进行操作,无法改变其值。

const arr = [ 1, 2, 3, 4, 5 ];

arr.forEach(ele => {
  ele = ele * 3;
});
console.log(arr); // [ 1, 2, 3, 4, 5 ]

对引用类型进行操作,可以改变其部分属性。

const objArr = [{
  name: 'wxw',
  age: 22,
}, {
  name: 'wxw2',
  age: 33,
}];

objArr.forEach(ele => {
  if (ele.name === 'wxw2') {
    ele.age = 88;
  }
});
console.log(objArr); // [ { name: 'wxw', age: 22 }, { name: 'wxw2', age: 88 } ]

对引用类型进行操作,无法改变整个当前item。

const changeItemArr = [{
  name: 'wxw',
  age: 22,
}, {
  name: 'wxw2',
  age: 33,
}];

changeItemArr.forEach(ele => {
  if (ele.name === 'wxw2') {
    ele = {
      name: 'change',
      age: 77,
    };
  }
});
console.log(changeItemArr); //[ { name: 'wxw', age: 22 }, { name: 'wxw2', age: 33 } ]

采用第三个指针参数,可以进行任意改变。


// 基本类型
const numArr = [ 33, 4, 55 ];
numArr.forEach((ele, index, arr) => {
  if (ele === 33) {
    arr[index] = 999;
  }
});
console.log(numArr); // [999, 4, 55]

// 引用类型
const allChangeArr = [{
  name: 'wxw',
  age: 22,
}, {
  name: 'wxw2',
  age: 33,
}];
allChangeArr.forEach((ele, index, arr) => {
  if (ele.name === 'wxw2') {
    arr[index] = {
      name: 'change',
      age: 77,
    };
  }
});
console.log(allChangeArr); // [{name: "wxw", age: 22},{name: "change", age: 77}]

promise、async、await 使用示例

forEach对promise不敏感,其会同时出发所有的promise对象,这样速度会快,但会造成并发。例如:查询数据库中百万条数据,同时触发百万个调用。

当采用promise、async、await作为forEach的callback函数时会出现执行顺序错误的问题。

const ratings = [ 5, 4, 5 ];
let sum = 0;

const sumFunction = async function(a, b) {
  return a + b;
};

ratings.forEach(async function(rating) {
  sum = await sumFunction(sum, rating);
});

console.log(sum);
// Expected output: 14
// Actual output: 0

总结

   过以上对比发现,`for ... of` 没有`for i`的繁琐语句,也不像`forEach`那样不支持异步和中断操作,其还可以对各种数据类型自定义迭代方式,功能强大,适用场景也多。通过结合数组的`entries()`、`keys()`、`values()`等方法,还可实现key键和value值的同时遍历,在修复`for ... in`若干Bug的基础上完美将其取代。

最后推荐大家遍历时首选for ... of,根据情况需要选择for i。多数情况下forEachfor...in可以被替代。