从零开始手撸数组三剑客:Map、Filter、Reduce 的「原生」实现

194 阅读4分钟

从零开始手撸数组三剑客:Map、Filter、Reduce 的「原生」实现

当面试官问你:"不用内置方法,你能实现 Array.map、Array.filter、Array.reduce 吗?" 别慌,这篇文章带你从容应对!

前言

作为一个前端开发者,你一定对 JavaScript 的数组方法如数家珍。 map 、 filter 、 reduce 这三个方法简直是数组操作的三剑客,日常开发中使用频率极高。但是,你有没有想过,如果让你不使用这些内置方法,从零开始实现它们,你会怎么做?

今天我们就来一场「回到原始时代」的编程之旅,用最朴素的 for 循环,手撸这三个经典方法的实现。

第一剑:Map - 数组变形记

题目描述

编写一个函数,接收一个整数数组 arr 和一个映射函数 fn ,返回一个新数组。新数组的创建规则: returnedArray[i] = fn(arr[i], i) 。

重点:不能使用内置的 Array.map 方法!

实现思路

map 方法的核心思想很简单:遍历原数组,对每个元素应用变换函数,将结果存入新数组。

var map = function(arr, fn) {
    const result = [];
    for (let i = 0; i < arr.length; i++) 
    {
        result[i] = fn(arr[i], i);
    }
    return result;
}

代码解析

  1. 创建新数组 : const result = [] - 我们不修改原数组,而是创建一个全新的数组
  2. 遍历原数组 :使用经典的 for 循环,从 0 到 arr.length - 1
  3. 应用变换函数 : fn(arr[i], i) - 传入当前元素和索引
  4. 存储结果 :直接通过索引赋值 result[i] = ... 这个实现简洁明了,完美复刻了 Array.map 的功能。

第二剑:Filter - 数组筛选器

题目描述

给定一个整数数组 arr 和一个过滤函数 fn ,返回一个过滤后的数组。只有当 fn(arr[i], i) 返回真值时, arr[i] 才会被包含在结果中。

重点:不能使用内置的 Array.filter 方法!

实现思路

filter 的逻辑是:遍历数组,对每个元素进行"审查",只有通过审查的元素才能进入结果数组。

var filter = function(arr, fn) {
    const result = [];
    for (let i = 0; i < arr.length; i++) 
    {
        if (fn(arr[i], i)) {
            result.push(arr[i]);
        }
    }
    return result;
}

关键点分析

  1. 条件判断 : if (fn(arr[i], i)) - 只有当过滤函数返回真值时才执行
  2. 动态添加 :使用 push 方法而不是索引赋值,因为结果数组的长度是不确定的
  3. 真值判断 :JavaScript 的真值包括非零数字、非空字符串、对象等

第三剑:Reduce - 数组聚合大师

题目描述

给定一个整数数组 nums 、一个 reducer 函数 fn 和一个初始值 init ,返回通过依次对数组的每个元素执行 fn 函数得到的最终结果。

执行过程: val = fn(init, nums[0]) → val = fn(val, nums[1]) → ... → 返回最终的 val

重点:不能使用内置的 Array.reduce 方法!

实现思路

reduce 是三剑客中最强大的,它可以将数组"压缩"成任何类型的单一值。

var reduce = function(nums, fn, init) {
    let val = init
    for (var i = 0; i < nums.length; i
    ++) {
        val = fn(val, nums[i])
    }
    return val
};

核心机制

  1. 累加器初始化 : let val = init - 从初始值开始
  2. 逐步聚合 :每次循环都更新累加器 val = fn(val, nums[i])
  3. 边界处理 :空数组直接返回初始值

使用示例

// 求和
const sum = reduce([1,2,3,4], (acc, 
curr) => acc + curr, 0); // 10

// 求平方和
const squareSum = reduce([1,2,3,4], 
(acc, curr) => acc + curr * curr, 
100); // 130

// 空数组
const empty = reduce([], (acc, curr) => 
025); // 25

性能对比与思考

时间复杂度

三个方法的时间复杂度都是 O(n) ,其中 n 是数组长度。这与原生方法保持一致。

空间复杂度

  • Map : O(n) - 需要创建新数组
  • Filter : O(k) - k 是满足条件的元素个数,最坏情况 O(n)
  • Reduce : O(1) - 只需要一个累加器变量

为什么要手写这些方法?

  1. 面试必备 :这是前端面试的经典题目
  2. 理解原理 :知其然更要知其所以然
  3. 调试能力 :当内置方法出现问题时,你能快速定位
  4. 扩展能力 :基于理解可以实现更复杂的变体

进阶思考

链式调用

原生数组方法支持链式调用,我们的实现也可以:

const result = map([1,2,3], x => x * 
2)  // [2,4,6]
    .filter(x => x > 
    3)                   // [4,6]
    .reduce((acc, curr) => acc + curr, 
    0); // 10

错误处理

生产环境中,我们还需要考虑:

  • 参数类型检查
  • 函数参数验证
  • 边界情况处理

总结

通过手写这三个经典方法,我们不仅复习了基础的循环和条件判断,更重要的是理解了函数式编程的核心思想:

  • Map : 变换 - 一对一映射
  • Filter : 筛选 - 条件过滤
  • Reduce : 聚合 - 多对一归约 这三个方法构成了数组操作的完整闭环,掌握了它们的原理,你就掌握了函数式编程的精髓。

下次面试官再问你这个问题时,你可以自信地说:"这不就是几个 for 循环的事儿嘛!" 😎

你还想了解哪些 JavaScript 内置方法的实现原理?欢迎在评论区留言讨论!