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 variablelet k = 0;// If the argument passed doesn't have a property len then forEach returnsif(!array.length)return;// checking if callback is callableif (typeof callback != 'function')return;// The user can set a custom this contextlet len = array.length;// iterating until k reaches the length of the array - 1while(k<len){// if the array doesn't have a k element at index k then we returnif(!array[k]){return;}let element = array[k];// notice the three elements used in the callbackcallback(element, k, array);// Increase k to reach the next item in the arrayk += 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 healthreasonableShoppingList.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 dishconst cheeseShopping = async (dishes) => {const whatCheeseShouldIUse = async (dish) => {// We use setTimeout to simulate an API callawait 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 looprequiredCheeses.push(recommendation)})// requiredCheeses = []// this await is uselessawait dishes.forEach( dish => {const recommendation = whatCheeseShouldIUse(dish);// Is a promise so we push a promise and not the result of the promiserequiredCheeses.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 codeawait callback(element, k, this);k += 1;};};
箭头函数解释了为什么我们需要使用function代替箭头函数。
const cheeseShopping = async (dishes) => {// ... Skipping some codeawait 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];// ...
