Array.prototype.forEach是ECMAScript 2015中引入的一个不错的小功能。它允许我们按顺序访问数组的每个元素。
基础
我们大多数人都知道字母,所以这里有个简单的例子:
const letters = ['a', 'b', 'c'];
letters.forEach((letter, index, arr) => {
console.log(letter,index, arr);
});
// The console will output
// 'a', 0, ['a', 'b', 'c']
// 'b', 1, ['a', 'b', 'c']
// 'c', 2, ['a', 'b', 'c']
深入
这里是ECMAScript文档中描述的回调流程的粗略的JavaScript实现。
const myForEach = (array, callback) => {
// Before iterating through the array forEach checks the value of array and sets a len variable
let k = 0;
// If the argument passed doesn't have a property len then forEach returns
if(!array.length)
return;
// checking if callback is callable
if (typeof callback != 'function')
return;
// The user can set a custom this context
let len = array.length;
// iterating until k reaches the length of the array - 1
while(k<len){
// if the array doesn't have a k element at index k then we return
if(!array[k]){
return;
}
let element = array[k];
// notice the three elements used in the callback
callback(element, k, array);
// Increase k to reach the next item in the array
k += 1;
}
// forEach never returns anything (return undefined is the same as return)
return undefined;
};
修改原始数组
从myForEach实现中可以看到,我们通过赋值获得element的值:
let element = array[k];
那么,如果我们修改元素会怎样?
const ruinYourElements = (element, index) => {
element = '乁( ◔ ౪◔)「 ';
}
const verySeriousArray = ['business', 'files', 'documents']
verySeriousArray.forEach(ruinYourElements)
// verySeriousArray = ['business', 'files', 'documents']`
// You failed to ruin my array
在这个片段中,元素从引用数组[k]到引用’乁( ◔ ౪◔)「 ‘。数组[k]永远不知道有这个重新分配。
但是,对象有些不同!
const ruiningYourNames = (element, index) => {
element.name = '乁( ◔ ౪◔)「 ';
}
const verySeriousArray = [{name:'business'}, {name:'files'}, {name:'documents'}];
verySeriousArray.forEach(ruiningYourNames);
// verySeriousArray = [{name: '乁( ◔ ౪◔)「 '}, {name: '乁( ◔ ౪◔)「 '}, {name: '乁( ◔ ౪◔)「 '}]
// You succeeded at ruining my array
会发生改变是因为元素仍然引用数组[k]。如果想防止这种行为,我们必须在myForEach中深度克隆数组[k]:
if(typeof array[k] === 'object'){
let element = JSON.parse(JSON.stringify(array[k]));
}
如果你想要更改原始数组中元素的值,则必须修改forEach回调函数中的第三个参数:arr:
const ruinYourArray = (element, index, arr) => {
arr[index] = '乁( ◔ ౪◔)「 ';
}
const verySeriousArray = ['business', 'files', 'documents']
verySeriousArray.forEach(ruinYourArray)
// verySeriousArray = ['乁( ◔ ౪◔)「 ', '乁( ◔ ౪◔)「 ', '乁( ◔ ౪◔)「 ']
// We successfully ruined the serious array, nobody will be able to do serious business anymore
循环是如何工作的
forEach将迭代初始数组的长度。如果数组的长度为5,则将迭代5次,但不会超过5次。
const reasonableShoppingList = ['🍈', '🥗'];
reasonableShoppingList.forEach((item)=> {
// Here is the 10 year old in me trying to highjack my health
reasonableShoppingList.push('🥞');
console.log(`bought ${item}`);
})
// console will output:
// bought 🍈 bought 🥗
// because forEach called the callback reasonableShoppingList.length = 2 times
// at the end reasonableShoppingList = ['🍈', '🥗', '🥞', '🥞'] so make sure to clean your array before you go shopping again!
在两种主要情况下迭代可以提前中断:
1.我们到达一个数组的点,而这个点已经不存在
const pop = (letter, index, arr) =>{
console.log(letter, i);
arr.pop();
}
letters.forEach(pop);
// 'a'
// 'b'
// letters = 'a'
修改数组时要小心!有时你会得到一些违反直觉的结果:
letters.forEach((letter, index, arr)=>{
console.log(letter, index);
if (letter === 'a')
arr.shift();
});
// 'a' 0
// 'c' 1
// letters = ['b','c']
查看myForEach,思考一下,这应该是有道理的。
2.如果回调函数崩溃
const showCity = (user) => {
console.log(user.address.city);
}
const users = [
{
name:'Sarah',
address:{
zipCode: 60633,
city: 'Chicago'
}
},
{
name:'Jack'
},
{
name:'Raphael',
address: {
city: 'ParadiseCity'
}
}
];
users.forEach(showCity);
// Console will output: 'Chicago'.Then we'll get:
// Uncaught TypeError: Cannot read property 'city' of undefined
在旧版本的浏览器中使用forEach
仍然有用户在使用不支持forEach的旧版浏览器。对于他们来说,最安全的选择是使用循环。但是,如果你想使用所有ECMA2015功能,你应使用polyfill或es5垫片。
forEach() vs map()
如你在myForEach中所见,forEach始终返回未定义,而map返回一个新数组。
异步forEach
如果你喜欢使用async and await,则有可能无法获得预期的行为。
// We are going to the cheese shop and ask the vendor what cheese we need for our dish
const cheeseShopping = async (dishes) => {
const whatCheeseShouldIUse = async (dish) => {
// We use setTimeout to simulate an API call
await new Promise(resolve => setTimeout(resolve, 200));
switch (dish) {
case 'Pasta':
return 'Parmesan'
case 'Gratin':
return 'Gruyère'
case 'Cheeseburger':
return 'American Cheese'
default:
return 'Tomme'
};
};
const requiredCheeses = [];
dishes.forEach( async (dish) => {
const recommendation = await whatCheeseShouldIUse(dish)
// We never reach this code because foreach doesn't wait for await and goes to the next loop
requiredCheeses.push(recommendation)
})
// requiredCheeses = []
// this await is useless
await dishes.forEach( dish => {
const recommendation = whatCheeseShouldIUse(dish);
// Is a promise so we push a promise and not the result of the promise
requiredCheeses.push(recommendation);
});
//requiredCheeses = [Promise, Promise, Promise]
};
}
const dishes = ['Pasta', 'Cheeseburger', 'Original Cheese Platter'];
cheeseShopping(dishes);
我们需要创建一个自定义的asyncForEach,它等待每个promise解析后才能继续。这是一个例子:
Array.prototype.asyncForEach = async function (callback) {
let k = 0;
while (k < this.length) {
if(!this[k])
return;
let element = this[k];
// This will pause the execution of the code
await callback(element, k, this);
k += 1;
};
};
箭头函数解释了为什么我们需要使用function代替箭头函数。
const cheeseShopping = async (dishes) => {
// ... Skipping some code
await dishes.asyncforEach( async dish => {
const recommendation = await whatCheeseShouldIUse(dish);
requiredCheeses.push(recommendation);
})
//requiredCheeses = ['Parmesan', 'American Cheese', 'Tomme']
return requiredCheeses;
};
有时(经常?),你可能想同时运行所有异步函数,并等待所有这些异步函数解析。Promise.all()在这种情况下可能非常有用。
性能
forEach循环比经典的for循环慢,但我们说的是百万元素数组中的微秒,所以不用担心。有趣的是,map和forEach的相对性能取决于你使用的浏览器版本,Chrome 61.0.3135(2)map更快 Chrome 61.0.3136(1)forEach更快。
DOM陷阱
小心!并非所有看起来像数组的都是数组:
const divs = document.getElementsByTagName('div');
divs.forEach(doSomething);
// Uncaught TypeError: divs.forEach is not a function
那是因为divs不是数组!这是一个称为DOMCollection的特殊对象,它是一个可迭代的对象。因此,你只能这样做:
for (let i = 0; i < divs.length; i++){
doSomething(divs[i], i);
}
或者打乱HTMLCollection的原型,添加一个forEach并强制其表现为原生的forEach:
HTMLCollection.prototype.forEach = Array.prototype.forEach;
或者先使用像spread这样的运算符,把类似数组的对象变成真正的数组:
const divs = document.getElementsByTagName('div');
const divsArr = [...divs];
// ...