Array.prototype.forEach是ECMAScript 2015中引入的一个不错的小功能。它允许我们按顺序访问数组的每个元素。

基础

我们大多数人都知道字母,所以这里有个简单的例子:

  1. const letters = ['a', 'b', 'c'];
  2. letters.forEach((letter, index, arr) => {
  3. console.log(letter,index, arr);
  4. });
  5. // The console will output
  6. // 'a', 0, ['a', 'b', 'c']
  7. // 'b', 1, ['a', 'b', 'c']
  8. // 'c', 2, ['a', 'b', 'c']

深入

这里是ECMAScript文档中描述的回调流程的粗略的JavaScript实现。

  1. const myForEach = (array, callback) => {
  2. // Before iterating through the array forEach checks the value of array and sets a len variable
  3. let k = 0;
  4. // If the argument passed doesn't have a property len then forEach returns
  5. if(!array.length)
  6. return;
  7. // checking if callback is callable
  8. if (typeof callback != 'function')
  9. return;
  10. // The user can set a custom this context
  11. let len = array.length;
  12. // iterating until k reaches the length of the array - 1
  13. while(k<len){
  14. // if the array doesn't have a k element at index k then we return
  15. if(!array[k]){
  16. return;
  17. }
  18. let element = array[k];
  19. // notice the three elements used in the callback
  20. callback(element, k, array);
  21. // Increase k to reach the next item in the array
  22. k += 1;
  23. }
  24. // forEach never returns anything (return undefined is the same as return)
  25. return undefined;
  26. };

修改原始数组

从myForEach实现中可以看到,我们通过赋值获得element的值:

  1. let element = array[k];

那么,如果我们修改元素会怎样?

  1. const ruinYourElements = (element, index) => {
  2. element = '乁( ◔ ౪◔)「 ';
  3. }
  4. const verySeriousArray = ['business', 'files', 'documents']
  5. verySeriousArray.forEach(ruinYourElements)
  6. // verySeriousArray = ['business', 'files', 'documents']`
  7. // You failed to ruin my array

在这个片段中,元素从引用数组[k]到引用’乁( ◔ ౪◔)「 ‘。数组[k]永远不知道有这个重新分配。

但是,对象有些不同!

  1. const ruiningYourNames = (element, index) => {
  2. element.name = '乁( ◔ ౪◔)「 ';
  3. }
  4. const verySeriousArray = [{name:'business'}, {name:'files'}, {name:'documents'}];
  5. verySeriousArray.forEach(ruiningYourNames);
  6. // verySeriousArray = [{name: '乁( ◔ ౪◔)「 '}, {name: '乁( ◔ ౪◔)「 '}, {name: '乁( ◔ ౪◔)「 '}]
  7. // You succeeded at ruining my array

会发生改变是因为元素仍然引用数组[k]。如果想防止这种行为,我们必须在myForEach中深度克隆数组[k]:

  1. if(typeof array[k] === 'object'){
  2. let element = JSON.parse(JSON.stringify(array[k]));
  3. }

如果你想要更改原始数组中元素的值,则必须修改forEach回调函数中的第三个参数:arr:

  1. const ruinYourArray = (element, index, arr) => {
  2. arr[index] = '乁( ◔ ౪◔)「 ';
  3. }
  4. const verySeriousArray = ['business', 'files', 'documents']
  5. verySeriousArray.forEach(ruinYourArray)
  6. // verySeriousArray = ['乁( ◔ ౪◔)「 ', '乁( ◔ ౪◔)「 ', '乁( ◔ ౪◔)「 ']
  7. // We successfully ruined the serious array, nobody will be able to do serious business anymore

循环是如何工作的

forEach将迭代初始数组的长度。如果数组的长度为5,则将迭代5次,但不会超过5次。

  1. const reasonableShoppingList = ['🍈', '🥗'];
  2. reasonableShoppingList.forEach((item)=> {
  3. // Here is the 10 year old in me trying to highjack my health
  4. reasonableShoppingList.push('🥞');
  5. console.log(`bought ${item}`);
  6. })
  7. // console will output:
  8. // bought 🍈 bought 🥗
  9. // because forEach called the callback reasonableShoppingList.length = 2 times
  10. // at the end reasonableShoppingList = ['🍈', '🥗', '🥞', '🥞'] so make sure to clean your array before you go shopping again!

在两种主要情况下迭代可以提前中断:
1.我们到达一个数组的点,而这个点已经不存在

  1. const pop = (letter, index, arr) =>{
  2. console.log(letter, i);
  3. arr.pop();
  4. }
  5. letters.forEach(pop);
  6. // 'a'
  7. // 'b'
  8. // letters = 'a'

修改数组时要小心!有时你会得到一些违反直觉的结果:

  1. letters.forEach((letter, index, arr)=>{
  2. console.log(letter, index);
  3. if (letter === 'a')
  4. arr.shift();
  5. });
  6. // 'a' 0
  7. // 'c' 1
  8. // letters = ['b','c']

查看myForEach,思考一下,这应该是有道理的。

2.如果回调函数崩溃

  1. const showCity = (user) => {
  2. console.log(user.address.city);
  3. }
  4. const users = [
  5. {
  6. name:'Sarah',
  7. address:{
  8. zipCode: 60633,
  9. city: 'Chicago'
  10. }
  11. },
  12. {
  13. name:'Jack'
  14. },
  15. {
  16. name:'Raphael',
  17. address: {
  18. city: 'ParadiseCity'
  19. }
  20. }
  21. ];
  22. users.forEach(showCity);
  23. // Console will output: 'Chicago'.Then we'll get:
  24. // Uncaught TypeError: Cannot read property 'city' of undefined

在旧版本的浏览器中使用forEach

仍然有用户在使用不支持forEach的旧版浏览器。对于他们来说,最安全的选择是使用循环。但是,如果你想使用所有ECMA2015功能,你应使用polyfill或es5垫片。

forEach() vs map()

如你在myForEach中所见,forEach始终返回未定义,而map返回一个新数组

异步forEach

如果你喜欢使用async and await,则有可能无法获得预期的行为。

  1. // We are going to the cheese shop and ask the vendor what cheese we need for our dish
  2. const cheeseShopping = async (dishes) => {
  3. const whatCheeseShouldIUse = async (dish) => {
  4. // We use setTimeout to simulate an API call
  5. await new Promise(resolve => setTimeout(resolve, 200));
  6. switch (dish) {
  7. case 'Pasta':
  8. return 'Parmesan'
  9. case 'Gratin':
  10. return 'Gruyère'
  11. case 'Cheeseburger':
  12. return 'American Cheese'
  13. default:
  14. return 'Tomme'
  15. };
  16. };
  17. const requiredCheeses = [];
  18. dishes.forEach( async (dish) => {
  19. const recommendation = await whatCheeseShouldIUse(dish)
  20. // We never reach this code because foreach doesn't wait for await and goes to the next loop
  21. requiredCheeses.push(recommendation)
  22. })
  23. // requiredCheeses = []
  24. // this await is useless
  25. await dishes.forEach( dish => {
  26. const recommendation = whatCheeseShouldIUse(dish);
  27. // Is a promise so we push a promise and not the result of the promise
  28. requiredCheeses.push(recommendation);
  29. });
  30. //requiredCheeses = [Promise, Promise, Promise]
  31. };
  32. }
  33. const dishes = ['Pasta', 'Cheeseburger', 'Original Cheese Platter'];
  34. cheeseShopping(dishes);

我们需要创建一个自定义的asyncForEach,它等待每个promise解析后才能继续。这是一个例子:

  1. Array.prototype.asyncForEach = async function (callback) {
  2. let k = 0;
  3. while (k < this.length) {
  4. if(!this[k])
  5. return;
  6. let element = this[k];
  7. // This will pause the execution of the code
  8. await callback(element, k, this);
  9. k += 1;
  10. };
  11. };

箭头函数解释了为什么我们需要使用function代替箭头函数。

  1. const cheeseShopping = async (dishes) => {
  2. // ... Skipping some code
  3. await dishes.asyncforEach( async dish => {
  4. const recommendation = await whatCheeseShouldIUse(dish);
  5. requiredCheeses.push(recommendation);
  6. })
  7. //requiredCheeses = ['Parmesan', 'American Cheese', 'Tomme']
  8. return requiredCheeses;
  9. };

有时(经常?),你可能想同时运行所有异步函数,并等待所有这些异步函数解析。Promise.all()在这种情况下可能非常有用。

性能

forEach循环比经典的for循环慢,但我们说的是百万元素数组中的微秒,所以不用担心。有趣的是,map和forEach的相对性能取决于你使用的浏览器版本,Chrome 61.0.3135(2)map更快 Chrome 61.0.3136(1)forEach更快。
loop_map_each_different_browers.png

DOM陷阱

小心!并非所有看起来像数组的都是数组:

  1. const divs = document.getElementsByTagName('div');
  2. divs.forEach(doSomething);
  3. // Uncaught TypeError: divs.forEach is not a function

那是因为divs不是数组!这是一个称为DOMCollection的特殊对象,它是一个可迭代的对象。因此,你只能这样做:

  1. for (let i = 0; i < divs.length; i++){
  2. doSomething(divs[i], i);
  3. }

或者打乱HTMLCollection的原型,添加一个forEach并强制其表现为原生的forEach:

  1. HTMLCollection.prototype.forEach = Array.prototype.forEach;

或者先使用像spread这样的运算符,把类似数组的对象变成真正的数组:

  1. const divs = document.getElementsByTagName('div');
  2. const divsArr = [...divs];
  3. // ...

原文链接:https://alligator.io/js/foreach-array-method/