JavaScript 神器 reduce():从入门到精通


🚀 JavaScript 神器 reduce():从入门到精通,彻底掌握数组的"降维打击"

嘿,各位前端同学!在我们的日常开发中,和数组打交道就像呼吸一样自然。我们用 forEach 遍历,用 map 转换,用 filter 筛选。但当需求变得复杂,比如"计算购物车商品总价"、"统计投票结果"或者"将二维数组展平"时,你是否曾用一连串的 for 循环和临时变量把自己绕晕?

别担心,今天我们要聊的,就是专门解决这类问题的"终极武器"------Array.prototype.reduce()

很多人对 reduce 的第一印象是:强大,但有点绕。它就像一把瑞士军刀,功能繁多,初见时不知从何下手。但一旦你理解了它的核心思想,你就会爱上这种将复杂问题优雅地"浓缩"成一行代码的快感。

这篇文章的目标,就是带你彻底撕掉 reduce "难懂"的标签。我们将不只停留在"会用"的层面,而是通过从零开始手写一个 myReduce ,让你真正洞悉其内部的每一个细节,成为真正的 reduce 大师!

💡 什么是 reduce

在我们动手造轮子之前,先快速回顾一下 reduce 的"官方定义"。

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

听起来有点抽象?没关系,看下它的语法结构:

javascript 复制代码
array.reduce(callbackFn, initialValue);

这里的关键是两个参数:

  1. callbackFn (回调函数) :这就是 reduce 的核心引擎,它会在数组的每个元素上运行一次。这个函数本身接收四个参数:

    • accumulator (累加器):灵魂角色!它是上一次回调函数执行后的返回值。它的初始值由 initialValue 决定。
    • currentValue (当前值):数组中正在被处理的那个元素。
    • currentIndex (当前索引):当前元素的索引。
    • array (源数组):调用 reduce() 的那个数组本身。
  2. initialValue (初始值) :可选参数,但极其重要!它决定了 accumulator 的"第一滴血"来自哪里。

    • 如果提供了 initialValueaccumulator 的初始值就是它,reduce 会从数组的第一个元素(索引 0)开始执行。
    • 如果未提供 initialValueaccumulator 会被初始化为数组的第一个元素reduce 则从数组的第二个元素(索引 1)开始执行。

是不是感觉清晰了一点?别急,当我们亲手实现它时,你会豁然开朗。

⚙️ 动手造轮子:从零实现一个 myReduce

理论讲千遍,不如代码敲一遍。现在,让我们卷起袖子,根据官方规范,一步步实现我们自己的 myReduce

javascript 复制代码
// filepath: c:\test\02-reduce.js

Array.prototype.myReduce = function (callbackFn, initialValue) {
  // 'this' 指向调用 myReduce 方法的数组实例。
  // 保存 this 是一个好习惯,能确保上下文正确,提高代码可读性。
  const arr = this;

  // 1. 边界检查:防御性编程是专业素养
  if (arr == null) {
    throw new TypeError("Array.prototype.myReduce called on null or undefined");
  }
  if (typeof callbackFn !== "function") {
    throw new TypeError(callbackFn + " is not a function");
  }

  let accumulator;
  let k = 0; // k 是用来遍历数组的索引

  // 2. 核心逻辑:确定 accumulator 的初始状态
  // arguments.length >= 2 是一个巧妙的方式,判断调用时是否传入了 initialValue
  if (arguments.length >= 2) {
    accumulator = initialValue;
  } else {
    // 当没有 initialValue 时,事情变得有趣了!
    // 我们必须找到数组中的第一个"真实"元素作为初始值。
    // 这段循环是为了跳过稀疏数组中的"空槽" (empty slots)。
    while (k < arr.length && !(k in arr)) {
      k++;
    }

    // 如果数组为空,且没有初始值,这是不允许的!
    if (k >= arr.length) {
      throw new TypeError("Reduce of empty array with no initial value");
    }

    // 将找到的第一个有效元素设为 accumulator,并从下一个元素开始遍历
    accumulator = arr[k];
    k++;
  }

  // 3. 主循环:遍历数组,执行回调
  while (k < arr.length) {
    // 再次使用 `k in arr` 确保我们只处理存在的元素,跳过空槽。
    if (k in arr) {
      // 这就是魔法发生的地方!
      // callbackFn 的返回值,会成为下一次循环的 accumulator。
      accumulator = callbackFn(accumulator, arr[k], k, arr);
    }
    k++;
  }

  // 4. 返回最终结果
  return accumulator;
};

代码剖析:

  • 稀疏数组与 in 操作符 :你可能会好奇 !(k in arr) 是什么操作。这是处理像 [1, , 3] 这种稀疏数组的关键。in 操作符可以准确地检测一个索引是否存在于数组中,而不是仅仅判断它是否为 undefined。这是许多原生数组方法(包括 reduce)在内部遵循的规范。
  • 密集数组(dense array),如 [10, 20, 30],可以看作是这样一个对象:{ "0": 10, "1": 20, "2": 30, "length": 3 }。
  • 稀疏数组(sparse array),如 [10, , 30],则可以看作是:{ "0": 10, "2": 30, "length": 3 }。
  • 在遍历多个元素的方法中,这些方法在访问索引之前执行 in 检查,并且不将空槽与 undefined 合并:concat()、copyWithin()、every()、filter()、flat()、flatMap()、forEach()、indexOf()、lastIndexOf()、map()、reduce()、reduceRight()、reverse()、slice()、some()、sort()、splice()。
  • 这些旧方法将空槽视为 undefined:entries()、fill()、find()、findIndex()、findLast()、findLastIndex()、includes()、join()、keys()、toLocaleString()、values()。
  • accumulator 的更新accumulator = callbackFn(...) 这一行是整个 reduce 的精髓。它形成了一个数据流:上一次计算的结果,无缝地流入下一次计算,像滚雪球一样,直到遍历结束。

reduce 的魔力:实用场景大揭秘

现在我们已经理解了 reduce 的内在机制,让我们用我们亲手打造的 myReduce 来见证它的强大威力吧!

场景一:数组求和(基础中的基础)
javascript 复制代码
const numbers = [1, 2, 3, 4, 5];

// ❌ 无 initialValue
const sumWithoutInitial = numbers.myReduce((accumulator, currentValue) => {
  return accumulator + currentValue;
});
console.log("总和 (无 initialValue):", sumWithoutInitial); // 输出: 15
// 过程: acc从1开始,依次加上2, 3, 4, 5

// ✅ 有 initialValue
const sumWithInitial = numbers.myReduce((accumulator, currentValue) => {
  return accumulator + currentValue;
}, 10);
console.log("总和 (有 initialValue):", sumWithInitial); // 输出: 25
// 过程: acc从10开始,依次加上1, 2, 3, 4, 5
场景二:二维数组展平("降维打击")
javascript 复制代码
const nestedArray = [[0, 1], [2, 3], [4, 5]];
const flattenedArray = nestedArray.myReduce(
  (accumulator, currentValue) => accumulator.concat(currentValue),
  [] // 初始值是一个空数组
);
console.log("展平后的数组:", flattenedArray); // 输出: [0, 1, 2, 3, 4, 5]

在这里,accumulator 从一个空数组开始,每次都把当前子数组合并进去,优雅地完成了展平操作。

场景三:统计数组元素出现次数
javascript 复制代码
const names = ["Alice", "Bob", "Tiff", "Bruce", "Alice"];
const countedNames = names.myReduce((allNames, name) => {
  const currCount = allNames[name] ?? 0; // 使用空值合并运算符简化代码
  return { ...allNames, [name]: currCount + 1 };
}, {}); // 初始值是一个空对象
console.log("名字出现次数统计:", countedNames);
// 输出: { Alice: 2, Bob: 1, Tiff: 1, Bruce: 1 }

这才是 reduce 真正施展魔法的地方!accumulator 不再是简单的数字或数组,而是一个对象,完美地实现了数据聚合。

场景四:按对象属性分组
javascript 复制代码
const people = [
  { name: "Alice", age: 21 },
  { name: "Max", age: 20 },
  { name: "Jane", age: 20 },
];
const groupedPeople = people.myReduce((acc, person) => {
  const key = person.age;
  const currentGroup = acc[key] ?? [];
  return { ...acc, [key]: [...currentGroup, person] };
}, {});
console.log("按年龄分组的人员:", groupedPeople);
// 输出:
// {
//   20: [ { name: 'Max', age: 20 }, { name: 'Jane', age: 20 } ],
//   21: [ { name: 'Alice', age: 21 } ]
// }

看到了吗?通过 reduce,我们可以轻松地将一个扁平的数组结构转换成一个复杂的、按需分组的对象结构。这比用 for 循环和一堆 if-else 判断要优雅得多!

🎯 总结:你现在是 reduce 大师了!

恭喜你!读到这里,你不仅学会了 reduce 的各种花式用法,更重要的是,你亲手构建了它的内核,理解了它每一行代码背后的逻辑。

现在,让我们回顾一下关键点:

  • reduce 是一个累加器:它的本质是将一个数组"浓缩"成一个单一的值(这个值可以是任何类型:数字、字符串、数组、对象)。
  • initialValue 至关重要 :它决定了 reduce 的起始状态,并能避免空数组报错,请大胆使用它!
  • accumulator 的流动是核心 :理解回调函数的返回值如何成为下一次调用的 accumulator,你就掌握了 reduce 的精髓。
  • 超越求和 :不要把 reduce 局限在数学计算上,它在数据塑形、转换和聚合方面的能力超乎你的想象。
相关推荐
爱喝水的小周8 分钟前
AJAX vs axios vs fetch
前端·javascript·ajax
Jinxiansen021110 分钟前
unplugin-vue-components 最佳实践手册
前端·javascript·vue.js
几道之旅14 分钟前
介绍electron
前端·javascript·electron
周胡杰16 分钟前
鸿蒙arkts使用关系型数据库,使用DB Browser for SQLite连接和查看数据库数据?使用TaskPool进行频繁数据库操作
前端·数据库·华为·harmonyos·鸿蒙·鸿蒙系统
315356691317 分钟前
ClipReader:一个剪贴板英语单词阅读器
前端·后端
玲小珑19 分钟前
Next.js 教程系列(十一)数据缓存策略与 Next.js 运行时
前端·next.js
qiyue7734 分钟前
AI编程专栏(三)- 实战无手写代码,Monorepo结构框架开发
前端·ai编程
断竿散人39 分钟前
JavaScript 异常捕获完全指南(下):前端框架与生产监控实战
前端·javascript·前端框架
Danny_FD40 分钟前
Vue2 + Vuex 实现页面跳转时的状态监听与处理
前端
小飞悟41 分钟前
别再只会用 px 了!移动端适配必须掌握的 CSS 单位
前端·css·设计