手撕JS数组三大方法:map、filter、reduce

0 阅读9分钟

在JavaScript中,mapfilterreduce 是三个非常重要的数组高阶方法。理解它们的手写实现,不仅有助于掌握数组遍历和回调函数的核心原理,也能加深对 JavaScript 函数式编程的理解。下面将分别给出三个方法的题目描述、相关文档说明以及完整的实现代码。

一、题目:FED9 Array.reduce

描述

请补全JavaScript代码,要求实现Array.reduce函数的功能且该新函数命名为"_reduce"。

输入描述:

[1,2,3]._reduce((left, right) => left + right)

输出描述:

6

示例1

输入:

[1,2,3]._reduce((left, right) => left + right)

输出:

6
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <style>
       /* 填写样式 */
    </style>
</head>

<body>
    <!-- 填写标签 -->
    <script type="text/javascript">
        // 填写JavaScript
    </script>
</body>

</html>

二、Array.prototype.reduce()

Array.prototype.reduce() - JavaScript | MDN

reduce() 方法对数组中的每个元素按序执行一个提供的 reducer 函数,每一次运行 reducer 会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值。

reduce() 的用处——计算数组所有元素的总和。

reduce(callbackFn)
reduce(callbackFn, initialValue)

JavaScript Array reduce() 方法

reduce 的核心思想:

遍历数组,每一步都把"当前累计结果"和"当前元素"按规则合并,产生新的累计结果,最后返回最终结果。

什么时候用 reduce

  • 需要把数组转换成非数组(对象、数字、字符串等)
  • 需要在一次遍历中做多件事(如:同时过滤和映射)
  • 需要依赖前一步的结果来决策(如:去重、分组)

什么时候不用 reduce

  • 简单遍历用 forEach
  • 一对一转换用 map
  • 条件筛选用 filter
  • 找第一个满足条件的用 find

定义和用法

reduce() 方法将数组缩减为单个值。

reduce() 方法为数组的每个值(从左到右)执行提供的函数。

函数的返回值存储在累加器中(结果/总计)。

**注释:**对没有值的数组元素,不执行 reduce() 方法。

注释:reduce() 方法不会改变原始数组。

三、解法

答案

初始输入没写initialValue ;

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <style>
       /* 填写样式 */
    </style>
</head>

<body>
    <!-- 填写标签 -->
    <script type="text/javascript">
       Array.prototype._reduce = function (Fn){
        if (this.length === 0 ){
            throw new TypeError();
        }
        let accumulator = this[0] ;

        for (let i = 1 ; i < this.length ;i++){
            accumulator = Fn(accumulator,this[i] , i , this);
        }
        return accumulator ;
       }
    </script>
</body>

</html>

我认为这个解法是标准一些的,结合MDN的默认参数来说。

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <style>
       /* 填写样式 */
    </style>
</head>

<body>
    <!-- 填写标签 -->
    <script type="text/javascript">
        Array.prototype._reduce = function (Fn , initialValue ) {
            if (this.length === 0 && initialValue === undefined){
                throw new TypeError();
            }

            let accumlator = initialValue !== undefined ? initialValue : this[0];
            let startIndex = initialValue !== undefined ? 0 : 1 ;

            for (let i = startIndex ; i < this.length ; i++){
                if (i in this ){
                    accumlator = Fn(accumlator , this[i] , i , this);
                }
            }
            return accumlator ;
        }
    </script>
</body>

</html>

一、题目:FED8 实现Array.filter函数的功能

描述

请补全JavaScript代码,要求实现Array.filter函数的功能且该新函数命名为"_filter"。

示例:

输入:[1,2]._filter(i => i>1) 输出:[2]

示例1

输入:

[1,2]._filter(i => i>1)

输出:

[2]
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <style>
       /* 填写样式 */
    </style>
</head>

<body>
    <!-- 填写标签 -->
    <script type="text/javascript">
        // 填写JavaScript
        Array.prototype._filter = function(Fn) {
   
}

    </script>
</body>

</html>

二、Array.prototype.filter()

Array.prototype.filter() - JavaScript | MDN

callback is called with three arguments: the value of the element, the index of the element, and the object being traversed.

回调函数被调用时传入三个参数:元素的值、元素的索引、被遍历的对象。

filter 的回调函数接收三个参数:

  • element:当前元素
  • index:当前索引
  • array:被遍历的数组
// 回调签名
function(element, index, array) { ... }

ECMA-262, 16th edition, June 2025ECMAScript® 2025 Language Specification

23.1.3.2.1 IsConcatSpreadable ( O )

抽象操作 IsConcatSpreadable 接受参数 O(一个 ECMAScript 语言值),并返回一个包含布尔值的正常完成记录或一个抛出完成记录。调用时执行以下步骤:

  1. 如果 O 不是对象,则返回 false
  2. 令 spreadable 为 ? Get(O, %Symbol.isConcatSpreadable%)。
  3. 如果 spreadable 不是 undefined,则返回 ToBoolean(spreadable)。
  4. 返回 ? IsArray(O)。

Function.prototype.call() - JavaScript | MDN

Function 实例的 call() 方法会以给定的 this 值和逐个提供的参数调用该函数。

  • fn.call(thisArg, arg1, arg2):“以 thisArgthis,调用 fn,再传 arg1、arg2

语法

call(thisArg)
call(thisArg, arg1)
call(thisArg, arg1, arg2)
call(thisArg, arg1, arg2, /* …, */ argN)

参数

返回值

使用指定的 this 值和参数调用函数后的结果。

thisArg是自己起的参数名,注意和this关键字要区分!!

MDN 中有示例

function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.call(this, name, price);
  this.category = "food";
}

console.log(new Food("cheese", 5).name);
// Expected output: "cheese"

这个例子完美展示了 call 的经典用法:借用(borrow)构造函数

逐行解析

function Product(name, price) {
  this.name = name;   // 这里的 this 指向谁?
  this.price = price; // 取决于调用方式
}

function Food(name, price) {
  Product.call(this, name, price);
  //            ↑     ↑     ↑
  //            |     |     └── 传给 Product 的第二个参数
  //            |     └──────── 传给 Product 的第一个参数
  //            └────────────── 把 Product 内部的 this 绑定到 Food 的 this
  this.category = "food";
}

关键步骤拆解

1. 当你执行 new Food("cheese", 5)

new Food("cheese", 5)

new 关键字做了几件事:

  • 创建一个空对象 {}
  • 把这个空对象绑定到 Food 函数内部的 this
  • 执行 Food 函数体

所以此时 Food 内部的 this 指向这个新对象:

// 进入 Food 函数时
this = {}  // 这个空对象

2. 执行 Product.call(this, name, price)

Product.call(this, "cheese", 5)
//            ↑
//            this 就是上面那个空对象 {}

这一行的意思是:this(即 Food 的新对象)作为 Product 内部的 this,去调用 Product 函数

3. Product 函数执行

function Product(name, price) {
  this.name = name;   // 这里的 this 就是 Food 的 this(同一个对象)
  this.price = price; // 所以等价于:foodObject.name = "cheese"
}

执行完后,那个原本空的对象变成了:

{ name: "cheese", price: 5 }

4. 继续执行 Food 的剩余代码

this.category = "food";

给同一个对象添加 category 属性:

{ name: "cheese", price: 5, category: "food" }

5. new 返回这个对象

所以 new Food("cheese", 5).name 输出 "cheese"


图解整个过程
new Food("cheese", 5)
        │
        ▼
   创建空对象 {}
        │
        ▼
   Food 内部 this = {}
        │
        ▼
   Product.call(this, "cheese", 5)
        │
        ▼
   Product 以 {} 作为 this 执行
        │
        ▼
   {}.name = "cheese"
   {}.price = 5
        │
        ▼
   回到 Food,继续执行
        │
        ▼
   {}.category = "food"
        │
        ▼
   最终对象:{ name: "cheese", price: 5, category: "food" }

总结:为什么要这样写?

不这样做这样做
Food 要重复写 this.name = name直接借用 Product 的逻辑,复用代码
Product 的代码改了,Food 要手动同步自动继承 Product 的所有属性初始化逻辑

一句话总结Product.call(this, ...) 的意思是 “借用 Product 函数来帮我初始化 this 这个对象的 nameprice 属性”,这样就不用自己重复写一遍了。

三、解法

注意,Fn是传入的测试用例函数,Fn(arr[i], i, arr) 里面的 iarr 就是占位参数

例如:

[1, 2, 3, 4]._filter(function(i) { return i > 2; });
//                          ↑
//                    这个函数被赋值给 Fn

function(i) { return i > 2; } 这个函数被赋值给 Fn

答案

传入thisArg

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <style>
       /* 填写样式 */
    </style>
</head>

<body>
    <!-- 填写标签 -->
    <script type="text/javascript">
        Array.prototype._filter = function(Fn,thisArg) {
            if (this == null ){
                throw new TypeError();
            }
            const arr = Object(this);
            const len = arr.length ;
            const res = [];

            for (let i = 0 ; i < len ; i ++){
                if (i in arr){
                    const passed = Fn.call(thisArg, arr[i] , i , arr);

                    if (passed){
                        res.push(arr[i]);
                    }
                }
            }
            return res ;
}


    </script>
</body>

</html>

不传入thisArg

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <style>
       /* 填写样式 */
    </style>
</head>

<body>
    <!-- 填写标签 -->
    <script type="text/javascript">
        Array.prototype._filter = function(Fn) {
            if (this == null ){
                throw new TypeError();
            }
            const arr = Object(this);
            const len = arr.length ;
            const res = [];

            for (let i = 0 ; i < len ; i ++){
                if (i in arr){
                   if (Fn(arr[i], i  , arr)){
                    res.push(arr[i]);
                   }
                }
            }
            return res ;
}


    </script>
</body>

</html>

一、题目:FED7 Array.map

描述

请补全JavaScript代码,要求实现Array.map函数的功能且该新函数命名为"_map"

示例:

输入:[1,2]._map(i => i * 2)

输出:[2,4]

示例1

输入:

[1,2]._map(i => i * 2)

输出:

[2,4]
<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <style>
       /* 填写样式 */
    </style>
</head>

<body>
    <!-- 填写标签 -->
    <script type="text/javascript">
        // 
        Array.prototype._map = function(Fn) {
    
}

    </script>
</body>

</html>

二、map()方法

Array.prototype.map() - JavaScript | MDN

ECMAScript® 2027 Language Specification

先去看看MDN介绍的用法和示例,还有规范步骤。

23.1.3.21 Array.prototype.map ( callback [ , thisArg ] )

Note 1

注释 1

callback should be a function that accepts three arguments. map calls callback once for each element in the array, in ascending order, and constructs a new Array from the results. callback is called only for elements of the array which actually exist; it is not called for missing elements of the array.

回调函数应当是一个接受三个参数的函数。map 方法按升序为数组中的每个元素调用一次回调函数,并根据每次调用的结果构建一个新数组。回调函数仅对数组中实际存在的元素调用;对于数组中缺失的元素(空位),不会调用回调函数。

If a thisArg parameter is provided, it will be used as the this value for each invocation of callback. If it is not provided, undefined is used instead.

如果提供了 thisArg 参数,它将被用作每次调用回调函数时的 this 值。如果未提供,则使用 undefined

callback is called with three arguments: the value of the element, the index of the element, and the object being traversed.

回调函数被调用时传入三个参数:元素的值、元素的索引、以及被遍历的对象。

map does not directly mutate the object on which it is called but the object may be mutated by the calls to callback.

map 方法不会直接修改调用它的对象,但该对象可能会被回调函数的调用所修改。

The range of elements processed by map is set before the first call to callback. Elements which are appended to the array after the call to map begins will not be visited by callback. If existing elements of the array are changed, their value as passed to callback will be the value at the time map visits them; elements that are deleted after the call to map begins and before being visited are not visited.

map 方法处理的元素范围是在第一次调用回调函数之前确定的。在 map 调用开始之后追加到数组中的元素不会被回调函数访问。如果数组中的现有元素被修改,则传递给回调函数的值将是 map 访问它们时的值;在 map 调用开始之后、被访问之前被删除的元素不会被访问。


This method performs the following steps when called:

该方法在调用时执行以下步骤:

  1. Let O be ? ToObject(this value).

    设 O 为 ? ToObject(this 值)。

  2. Let len be ? LengthOfArrayLike(O).

    设 len 为 ? LengthOfArrayLike(O)。

  3. If IsCallable(callback) is false, throw a TypeError exception.

    如果 IsCallable(callback)为 false,则抛出 TypeError 异常。

  4. Let A be ? ArraySpeciesCreate(O, len).

    设 A 为 ? ArraySpeciesCreate(O,len)。

  5. Let k be 0.

    设 k 为 0。

  6. Repeat, while k < len,

    重复,当 k < len 时:

    a. Let Pk be ! ToString(𝔽(k)). 设 Pk 为 ! ToString(𝔽(k))。

    b. Let kPresent be ? HasProperty(O, Pk). 设 kPresent 为 ? HasProperty(O,Pk)。

    c. If kPresent is true, then 如果 kPresent 为 true,则:

    i. Let kValue be ? Get(O, Pk). 设 kValue 为 ? Get(O,Pk)。

    ii. Let mappedValue be ? Call(callback, thisArg, « kValue, 𝔽(k), O »). 设 mappedValue 为 ? Call(callback,thisArg,« kValue,𝔽(k),O »)。

    iii. Perform ? CreateDataPropertyOrThrow(A, Pk, mappedValue). 执行 ? CreateDataPropertyOrThrow(A,Pk,mappedValue)。

    d. Set k to k + 1. 将 k 设为 k + 1。

  7. Return A.

    返回 A。


Note 2

注释 2

This method is intentionally generic; it does not require that its this value be an Array. Therefore it can be transferred to other kinds of objects for use as a method.

该方法是故意设计为通用的;它不要求其 this 值必须是一个数组。因此,它可以被转移到其他类型的对象上作为方法使用。

  1. 创建一个空数组
  2. 遍历原数组的每个元素
  3. 对每个元素执行回调函数
  4. 把回调的返回值放进新数组
  5. 返回新数组
// 用大白话翻译 map 的工作流程
function 手动实现Map(原数组, 回调函数) {
    let 新数组 = [];
    for (let i = 0; i < 原数组.length; i++) {
        let 处理后的值 = 回调函数(原数组[i], i, 原数组);
        新数组.push(处理后的值);
    }
    return 新数组;
}

三、解法

答案

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <style>
       /* 填写样式 */
    </style>
</head>

<body>
    <!-- 填写标签 -->
    <script type="text/javascript">
        // 
        Array.prototype._map = function(Fn) {
            let newArr = [];
            for (let i = 0 ; i < this.length ; i++){
                let val = Fn(this[i] , i , this);
                newArr[i] = val ;
            }
            return newArr ;
}

    </script>
</body>

</html>

总结

以上完整呈现了 _map_filter_reduce 三个方法的题目要求、文档说明和多种实现方式,建议结合 MDN 文档和规范步骤反复练习。