在 JavaScript 中,数组是一个非常常用的数据结构。我们每天都在用 map、filter、forEach 这些方法来处理数据。但很多人只是会用,却不清楚它们到底是怎么工作的。今天,我们就来彻底搞明白这些方法背后的机制,不靠黑盒,而是自己动手,从零开始实现它们。
我们要做的,是向 Array.prototype 添加一些我们自己定义的方法。比如 my_foreach、my_map、my_Filter、my_Every 和 myReduce。通过这种方式,我们不仅能理解这些方法的行为,还能掌握 JavaScript 中一个非常重要的概念:原型链与 this 的指向。
一、forEach的实现
Array.prototype.my_foreach = function(callback) {
for (let i = 0; i < this.length; i++) {
callback(this[i], i, this);
}
};
这段代码做了什么?我们一行一行来看。
第一行:
Array.prototype.my_foreach = function(callback) {
这表示我们正在向 Array 构造函数的原型对象上添加一个名为 my_foreach 的函数。Array.prototype 是所有数组实例的共同原型。也就是说,只要你创建一个数组,无论是用字面量 [] 还是 new Array(),它都会继承 Array.prototype 上的所有属性和方法。
所以,当我们在这里添加一个 my_foreach 方法后,所有的数组实例就都能直接调用它了。比如:
const arr = [10, 20, 30];
arr.my_foreach((item, index, array) => {
console.log(item, index);
});
这段代码会正常运行,输出每个元素和它的索引。为什么?因为 arr 虽然没有自己定义 my_foreach,但它可以通过原型链找到 Array.prototype 上的这个方法。
接下来,函数接收一个参数 callback。这个 callback 不是一个普通的值,而是一个函数。我们在调用 my_foreach 的时候,会传入一个函数作为参数,这个函数会被 my_foreach 内部调用。
进入函数体:
for (let i = 0; i < this.length; i++) {
callback(this[i], i, this);
}
这里是一个 for 循环,从索引 0 开始,一直遍历到 this.length - 1。关键点在于:这里的 this 指向谁?
在 JavaScript 中,函数内部的 this 取决于它是如何被调用的。当我们写 arr.my_foreach(...) 时,my_foreach 是作为 arr 的一个方法被调用的。因此,在这个函数执行时,this 就指向 arr 这个数组实例。
所以:
this[i]就是数组在位置i上的元素;i是当前的索引;this是数组本身。
然后我们调用 callback,并把这三个值作为参数传进去。这完全模仿了原生 forEach 方法的参数顺序:元素、索引、数组。
这个方法不会返回任何有意义的值(默认返回 undefined),它的目的不是生成新数据,而是对数组中的每一项执行某个操作,比如打印、修改 DOM、发送请求等。这就是所谓的“副作用操作”。
需要注意的是,这个实现不会跳过空元素(虽然在稀疏数组中可能表现不同),也不会因为 callback 中的 return 而中断循环。如果想支持中断,需要额外的逻辑,但原生 forEach 本身也不支持中断,所以这个行为是一致的。
二、 map 的实现:
Array.prototype.my_map = function(callback) {
const result = [];
for (let i = 0; i < this.length; i++) {
result.push(callback(this[i], i, this));
}
return result;
};
这个方法的目标是创建一个新数组,新数组的每个元素是原数组对应元素经过 callback 处理后的返回值。
我们先创建一个空数组 result,然后遍历原数组的每一项,把 callback 的返回值 push 进 result。最后返回这个新数组。
重点是:它不会修改原数组,而是返回一个全新的数组。这符合函数式编程中“不可变性”的原则。
比如:
[1, 2, 3].my_map(x => x * 2); // 返回 [2, 4, 6]
每个元素都被映射成了一个新的值。
三、Filter:
Array.prototype.my_Filter = function(callback) {
const result = [];
for (let i = 0; i < this.length; i++) {
if (callback(this[i], i, this)) {
result.push(this[i]);
}
}
return result;
};
或者写成简洁形式:
callback(this[i], i, this) && result.push(this[i]);
这种写法利用了逻辑与 && 的短路特性:只有当左边为 true 时,右边才会执行。所以如果 callback 返回 true,就 push 元素;否则不执行。
它的作用是筛选出所有让 callback 返回 true 的元素,组成一个新数组。
比如:
[1, 2, 3, 4, 5].my_Filter(x => x % 2 === 0); // [2, 4]
四、然后是 Every:
Array.prototype.my_Every = function(callback) {
for (let i = 0; i < this.length; i++) {
if (!callback(this[i], i, this)) {
return false;
}
}
return true;
};
这个方法用来判断数组中的每一个元素是否都满足某个条件。只要有一个元素让 callback 返回 false,整个方法就立刻返回 false。只有全部通过,才返回 true。
它的执行是“短路”的,一旦发现不满足的项,就不再继续遍历,提高了效率。
比如判断数组是否全是正数:
[1, 2, 3].my_Every(x => x > 0); // true
五、myReduce:
Array.prototype.myReduce = function (callback,initial){
if(!(callback instanceof Function)){
throw new TypeErroe('callback must be a function')
}
let start = 0;
let prev ;
if(arguments.length>=2){
prev = initial;
}else{
prev = this[0];
start = 1;
}
let n = this.length;
for(let i=start;i<n;i++){
prev = callback(prev,this[i],i,this);
}
return prev;
}
reduce 的作用是将数组“归约”成一个单一的值。它可以用来求和、拼接字符串、扁平化数组、统计频率等等。
它的核心是“累加器”的概念。previousValue 就是上一次调用 callback 的返回值,第一次的时候,它可能是你传入的初始值,也可能是数组的第一个元素。
...arg 是剩余参数语法,用来收集除了第一个参数 callback 之外的所有参数。我们用它来判断是否传入了初始值。
如果传了初始值,就从索引 0 开始遍历,previousValue 就是初始值;如果没有传,就用数组第一个元素作为初始值,并从第二个元素开始遍历。
然后在循环中不断调用 callback,把上一次的结果、当前元素、索引和数组本身传进去,直到遍历结束,最后返回最终的“累计值”。
比如:
[1, 2, 3, 4].myReduce((sum, item) => sum + item); // 10