从零开始手撸数组三剑客: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;
}
代码解析
- 创建新数组 : const result = [] - 我们不修改原数组,而是创建一个全新的数组
- 遍历原数组 :使用经典的 for 循环,从 0 到 arr.length - 1
- 应用变换函数 : fn(arr[i], i) - 传入当前元素和索引
- 存储结果 :直接通过索引赋值 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;
}
关键点分析
- 条件判断 : if (fn(arr[i], i)) - 只有当过滤函数返回真值时才执行
- 动态添加 :使用 push 方法而不是索引赋值,因为结果数组的长度是不确定的
- 真值判断 :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
};
核心机制
- 累加器初始化 : let val = init - 从初始值开始
- 逐步聚合 :每次循环都更新累加器 val = fn(val, nums[i])
- 边界处理 :空数组直接返回初始值
使用示例
// 求和
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) =>
0, 25); // 25
性能对比与思考
时间复杂度
三个方法的时间复杂度都是 O(n) ,其中 n 是数组长度。这与原生方法保持一致。
空间复杂度
- Map : O(n) - 需要创建新数组
- Filter : O(k) - k 是满足条件的元素个数,最坏情况 O(n)
- Reduce : O(1) - 只需要一个累加器变量
为什么要手写这些方法?
- 面试必备 :这是前端面试的经典题目
- 理解原理 :知其然更要知其所以然
- 调试能力 :当内置方法出现问题时,你能快速定位
- 扩展能力 :基于理解可以实现更复杂的变体
进阶思考
链式调用
原生数组方法支持链式调用,我们的实现也可以:
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 内置方法的实现原理?欢迎在评论区留言讨论!