在 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, ...arg) {
let previousValue;
let startIndex = 0;
if (arg.length > 0) {
previousValue = arg[0];
} else {
previousValue = this[0];
startIndex = 1;
}
for (let i = startIndex; i < this.length; i++) {
previousValue = callback(previousValue, this[i], i, this);
}
return previousValue;
};
reduce
的作用是将数组“归约”成一个单一的值。它可以用来求和、拼接字符串、扁平化数组、统计频率等等。
它的核心是“累加器”的概念。previousValue
就是上一次调用 callback
的返回值,第一次的时候,它可能是你传入的初始值,也可能是数组的第一个元素。
...arg
是剩余参数语法,用来收集除了第一个参数 callback
之外的所有参数。我们用它来判断是否传入了初始值。
如果传了初始值,就从索引 0
开始遍历,previousValue
就是初始值;如果没有传,就用数组第一个元素作为初始值,并从第二个元素开始遍历。
然后在循环中不断调用 callback
,把上一次的结果、当前元素、索引和数组本身传进去,直到遍历结束,最后返回最终的“累计值”。
比如:
[1, 2, 3, 4].myReduce((sum, item) => sum + item); // 10