共计 3285 个字符,预计需要花费 9 分钟才能阅读完成。
你了解的 Array.prototype.forEach
真的对吗?
Array.prototype.forEach
咱们都晓得,forEach()
办法对数组的每个元素执行一次给定的函数。它的语法也很简略:arr.forEach(callback(currentValue [, index [, array]])[, thisArg])
:
-
callback:为数组中每个元素执行的函数,该函数接管一至三个参数:
- currentValue:数组中正在解决的以后元素。
- index 可选,数组中正在解决的以后元素的索引。
- array 可选,forEach() 办法正在操作的数组。
- thisArg 可选参数。当执行回调函数 callback 时,用作 this 的值。
- 返回值:
undefined
罕用用法:
const array1 = ['a', 'b', 'c'];
array1.forEach((element) => console.log(element)); // 输入:a,b,c
相比一般的 for 循环,forEach 无需本人管制循环条件,所以很多时候,forEach 办法被用来代替 for 循环来实现数组的遍历。
这个 forEach 的实现真的对吗?
因为很多时候,forEach 办法被用来代替 for 循环来实现数组的遍历,所以常常能够看见 forEach 的一些 js 实现,例如:
Array.prototype.forEachCustom = function (fn, context) {context = context || arguments[1];
if (typeof fn !== 'function') {throw new TypeError(fn + 'is not a function');
}
for (let i = 0; i < this.length; i++) {fn.call(context, this[i], i, this);
}
};
看起来没有问题,咱们测试一下:
const items = ['item1', 'item2', 'item3'];
items.forEach((item) => {console.log(item); // 顺次打印:item1,item2,item3
});
items.forEachCustom((item) => {console.log(item); // 顺次打印:item1,item2,item3
});
好的,仿佛没有问题,所有貌似都很完满。咱们再测试下上面几个示例:
// 示例 1
const items = ['','item2','item3', , undefined, null, 0];
items.forEach((item) => {console.log(item); // 顺次打印:'',item2,item3,undefined,null,0
});
items.forEachCustom((item) => {console.log(item); // 顺次打印:'',item2,item3,undefined,undefined,null,0
});
// 示例 2
let arr = new Array(8);
arr.forEach((item) => {console.log(item); // 无打印输出
});
arr[1] = 9;
arr[5] = 3;
arr.forEach((item) => {console.log(item); // 打印输出:9 3
});
arr.forEachCustom((item) => {console.log(item); // 打印输出:daundefined 9 undefined*3 3 undefined*2
});
貌似产生了什么可怕的事儿,同样的数组通过 forEachCustom 和 forEach 调用,在打印出的值和值的数量上均有差异。看来我认为的并不真的就是我认为的。
寻根究底
怎么办呢?咱无妨去看看 ECMA 文档,看看 forEach 是怎么实现的:
咱们能够发现,真正执行遍历操作的是第 8 条,通过一个 while 循环来实现,循环的终止条件是后面获取到的数组的长度(也就是说前期扭转数组长度不会影响遍历次数),while 循环里,会先把以后遍历项的下标转为字符串,通过 HasProperty 办法判断数组对象中是否有下标对应的已初始化的项,有的话,获取对应的值,执行回调,没有的话,不会执行回调函数,而是间接遍历下一项。
如此看来,forEach 不对未初始化的值进行任何操作(稠密数组),所以才会呈现示例 1 和示例 2 中自定义办法打印出的值和值的数量上均有差异的景象。那么,咱们只需对后面的实现稍加革新,即可实现一个本人的 forEach 办法:
Array.prototype.forEachCustom = function (fn, context) {context = context || arguments[1];
if (typeof fn !== 'function') {throw new TypeError(fn + 'is not a function');
}
let len = this.length;
let k = 0;
while (k < len) {if (this.hasOwnProperty(k)) {fn.call(context, this[k], k, this);
}
k++;
}
};
再次运行示例 1 和示例 2 的测试用列,发现输入和原生 forEach 统一。
通过文档,咱们还发现,在迭代前 while 循环的次数就曾经定了,且执行了 while 循环,不代表就肯定会执行回调函数,咱们尝试在迭代时批改数组:
// 示例 3
var words = ['one', 'two', 'three', 'four'];
words.forEach(function (word) {console.log(word); // one,two,four(在迭代过程中删除元素,导致 three 被跳过,因为 three 的下标曾经变成 1,而下标为 1 的曾经被遍历了过)if (word === 'two') {words.shift();
}
});
words = ['one', 'two', 'three', 'four']; // 从新初始化数组进行 forEachCustom 测试
words.forEachCustom(function (word) {console.log(word); // one,two,four
if (word === 'two') {words.shift();
}
});
// 示例 4
var arr = [1, 2, 3];
arr.forEach((item) => {if (item == 2) {arr.push(4);
arr.push(5);
}
console.log(item); // 1,2,3(迭代过程中在开端减少元素,并不会减少迭代次数)});
arr = [1, 2, 3];
arr.forEachCustom((item) => {if (item == 2) {arr.push(4);
arr.push(5);
}
console.log(item); // 1,2,3
});
番外篇
除了抛出异样以外,没有方法停止或跳出 forEach() 循环。如果你须要停止或跳出循环,forEach() 办法不是该当应用的工具。若你须要提前终止循环,你能够应用:
- 一个简略的 for 循环
- for…of / for…in 循环
- Array.prototype.every()
- Array.prototype.some()
- Array.prototype.find()
- Array.prototype.findIndex()
这些数组办法则能够对数组元素判断,以便确定是否须要持续遍历:
- every()
- some()
- find()
- findIndex()
总结
- forEach 不对未初始化的值进行任何操作(稠密数组);
- 在迭代前,循环的次数就曾经定了,且执行了循环,不代表就肯定会执行回调函数;
- 除了抛出异样以外,没有方法停止或跳出 forEach() 循环。
遇到问题不可怕,多看文档,你总是会有不一样的播种。附 ECMA 文档地址:
- ecma-262/6.0
- ecma-262/11.0
本文首发于集体博客,欢送斧正和 star。