SSF0SSF0
首页
前端
  • Node
  • Go
  • Python
  • MySql
  • Bash
  • Git
  • Docker
  • VuePress
  • CI/CD
  • 服务器
  • 网站
  • 学习资料
  • 软件
  • 开源项目
Timeline
Github
标签
分类
首页
前端
  • Node
  • Go
  • Python
  • MySql
  • Bash
  • Git
  • Docker
  • VuePress
  • CI/CD
  • 服务器
  • 网站
  • 学习资料
  • 软件
  • 开源项目
Timeline
Github
标签
分类
  • HTML

    • html1
    • html2
  • CSS

    • Flex 布局常见问题与解决方案
  • JavaScript

    • 数据类型及引用问题
    • 处理 Blob 类型文件下载问题总结
    • localStorage 与 sessionStorage 区别
    • JavaScript 中的 script 标签问题详解
    • JavaScript 中的 this 指向问题详解
    • SessionStorage踩坑记录
    • 动态加载 JS 脚本方法对比
    • 浏览器页面关闭场景下的数据上报
    • HTTP流式接口详解与封装实践
    • JavaScript 中 this 绑定与解构:为什么 Pinia Store 可以解构,但 Class 实例方法不行?
  • es6

    • Promise
    • es6 模块导出方式全解析
    • reduce
    • forEach 中使用 async/await 的异步陷阱
  • Vue2

    • created VS mounted 发起异步请求
    • vue2-2
  • Vue3

    • Vite + Vue3 修改 element-plus 源码
    • Vue v-if 与 v-show
    • Vue3 ref 获取组件
    • Vue3 路由传参
    • 父子组件与组件里 Hooks 加载顺序
    • 第三方组件传入参数TS提示
    • Vue3 Props 在 Hooks 中的响应性处理
    • Vue Router 的两种历史模式及部署配置详解
    • 在Vue 3项目中顺利集成Tailwind CSS的完整指南
    • Vue Scoped CSS 深度解析
    • Vue 3 Teleport 组件完全指南:从原理到实战
    • Vant List 组件中双向绑定 loading 的注意事项
    • Vue 3 + Vite 中的路径别名与静态资源处理深度解析
    • 获取图片高度
    • 深入理解 Pinia:storeToRefs vs toRefs - 避开 TypeScript 类型推断的坑
  • webSocket

    • WebSocket 鉴权入门(实战版):握手票据 / 首帧鉴权 + 连接级 cid
  • Electron

    • 快速构建 electron + vue3 项目
  • TS

    • TS 泛型
    • 记录模板使用断言的问题
    • type 与 interface
  • WebPack

    • Webpack 介绍
  • Vite

    • Vite CLI 常见命令
    • vite 与 webpack 比较
    • Vite base 配置与 public 静态资源路径问题
  • 客户端数据库

    • IndexedDB 索引陷阱:字段重命名后索引返回旧数据
    • IndexedDB 事务陷阱与最佳实践:TransactionInactiveError 完全解析
  • UniApp

    • uni-app + Monorepo + 微信小程序热更新失效问题排查与结论总结
    • Monorepo + uni-app 开发中监听 / 热更新机制差异与解决方案
    • uni-app 运行与编译原理梳理
    • 为什么 uni-app 在 Monorepo 中必须做「环境区分」,而 Web 项目却不用?
  • 项目工程

    • 前端代码风格
    • Vue3 项目规范
    • npm 镜像问题
    • 包管理工具
    • 使用 engines 限制开发环境依赖
    • 打包与shell交互指定模式
    • Monorepo示例
    • PC 项目兼容 uni-app:基于 pnpm Monorepo 的工程化重构实践
    • Vue3 + Vite + pnpm Monorepo 迁移问题指南
    • 前端工程化:深入理解 npm 依赖管理机制
    • pnpm 修改依赖源码打包报错
    • 代码片段
  • 移动端

    • 浏览器跳转外部应用时请求状态码问题
  • 记录一下小程序
  • 控制浏览器正确保存网站账号密码的技巧
  • 信息泄露
  • 使用Gulp实现前端项目哈希化压缩打包
  • 前端缓存策略与文件哈希化的关联解析
  • 浏览器刷新方式与 Vue 生命周期、缓存行为解析
  • 浏览器消息铃声实现记录

reduce 基础·进阶·高阶用法

基础语法

array.reduce(function(total, currentValue, currentIndex, arr), initialValue);

参数说明:

  • total: 必需。累积器,初始值或者上一次回调的返回值

  • currentValue: 必需。当前正在处理的数组元素

  • currentIndex: 可选。当前元素的索引

  • arr: 可选。当前元素所属的数组对象

  • initialValue: 可选。传递给函数的初始值

实现原理

// 手动实现 Array.prototype.reduce
Array.prototype.myReduce = function (callback, initialValue) {
  // 1. 检查回调函数是否为函数
  if (typeof callback !== "function") {
    throw new TypeError("Callback must be a function");
  }

  // 2. 获取数组长度和起始索引
  const array = this;
  const length = array.length;
  let accumulator;
  let startIndex;

  // 3. 处理初始值
  if (arguments.length >= 2) {
    // 有初始值,从索引 0 开始
    accumulator = initialValue;
    startIndex = 0;
  } else {
    // 无初始值,检查数组是否为空
    if (length === 0) {
      throw new TypeError("Reduce of empty array with no initial value");
    }
    // 使用数组第一个元素作为初始值,从索引 1 开始
    accumulator = array[0];
    startIndex = 1;
  }

  // 4. 遍历数组,执行回调函数
  for (let i = startIndex; i < length; i++) {
    // 跳过稀疏数组的空位
    if (i in array) {
      accumulator = callback(accumulator, array[i], i, array);
    }
  }

  // 5. 返回累积结果
  return accumulator;
};

// 测试自定义实现
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.myReduce((acc, cur) => acc + cur, 0);
console.log(sum); // 15

执行流程详解

让我们通过一个具体的例子来理解 reduce 的执行过程:

const arr = [1, 2, 3, 4];
const result = arr.reduce((acc, cur, index) => {
  console.log(`第 ${index + 1} 次执行:`);
  console.log(`  累积值(acc): ${acc}`);
  console.log(`  当前值(cur): ${cur}`);
  console.log(`  返回值: ${acc + cur}`);
  console.log("---");
  return acc + cur;
}, 0);

/*
输出:
第 1 次执行:
  累积值(acc): 0
  当前值(cur): 1
  返回值: 1
---
第 2 次执行:
  累积值(acc): 1
  当前值(cur): 2
  返回值: 3
---
第 3 次执行:
  累积值(acc): 3
  当前值(cur): 3
  返回值: 6
---
第 4 次执行:
  累积值(acc): 6
  当前值(cur): 4
  返回值: 10
---

最终结果: 10
*/

关键概念解析

1. 累积器(Accumulator)

// 累积器保存每次回调的返回值
const steps = [];
[1, 2, 3].reduce((acc, cur) => {
  const newAcc = acc + cur;
  steps.push({ acc, cur, newAcc });
  return newAcc;
}, 0);

console.log(steps);
/*
[
  { acc: 0, cur: 1, newAcc: 1 },
  { acc: 1, cur: 2, newAcc: 3 },
  { acc: 3, cur: 3, newAcc: 6 }
]
*/

2. 初始值的影响

const arr = [1, 2, 3];

// 有初始值:从第一个元素开始遍历
const withInitial = arr.reduce((acc, cur, index) => {
  console.log(`索引 ${index}: acc=${acc}, cur=${cur}`);
  return acc + cur;
}, 10);
/*
索引 0: acc=10, cur=1
索引 1: acc=11, cur=2
索引 2: acc=13, cur=3
结果: 16
*/

// 无初始值:从第二个元素开始遍历
const withoutInitial = arr.reduce((acc, cur, index) => {
  console.log(`索引 ${index}: acc=${acc}, cur=${cur}`);
  return acc + cur;
});
/*
索引 1: acc=1, cur=2
索引 2: acc=3, cur=3
结果: 6
*/

边界情况处理

// 1. 空数组处理
try {
  [].reduce((acc, cur) => acc + cur); // 抛出错误
} catch (e) {
  console.log("空数组必须提供初始值");
}

const emptyResult = [].reduce((acc, cur) => acc + cur, 0); // 返回 0

// 2. 稀疏数组处理
const sparse = [1, , 3, , 5]; // 包含空位的数组
const sparseSum = sparse.reduce((acc, cur, index) => {
  console.log(`处理索引 ${index}: ${cur}`);
  return acc + cur;
}, 0);
/*
处理索引 0: 1
处理索引 2: 3
处理索引 4: 5
结果: 9 (跳过了空位)
*/

// 3. 单元素数组
const single = [42];
const singleResult = single.reduce((acc, cur) => acc + cur);
console.log(singleResult); // 42 (直接返回该元素,不执行回调)

基础用法

实例一:数组求和

const numbers = [1, 2, 3, 4, 5];

// 基础求和
const sum = numbers.reduce((total, current) => total + current, 0);
console.log(sum); // 15

// 不提供初始值(从数组第二个元素开始)
const sum2 = numbers.reduce((total, current) => total + current);
console.log(sum2); // 15

实例二:数组求最大值

const numbers = [5, 2, 8, 1, 9, 3];

const max = numbers.reduce((prev, current) => {
  return current > prev ? current : prev;
});
console.log(max); // 9

实例三:字符串拼接

const words = ["Hello", "World", "JavaScript"];

const sentence = words.reduce((acc, word) => acc + " " + word, "");
console.log(sentence.trim()); // "Hello World JavaScript"

进阶用法

实例四:数组扁平化

const nestedArray = [
  [1, 2],
  [3, 4],
  [5, 6],
];

const flattened = nestedArray.reduce((acc, current) => acc.concat(current), []);
console.log(flattened); // [1, 2, 3, 4, 5, 6]

// 处理多层嵌套
const deepNested = [1, [2, [3, 4]], 5];
const flatten = (arr) => {
  return arr.reduce((acc, val) => {
    return acc.concat(Array.isArray(val) ? flatten(val) : val);
  }, []);
};
console.log(flatten(deepNested)); // [1, 2, 3, 4, 5]

实例五:统计数组元素出现次数

const fruits = ["apple", "banana", "apple", "orange", "banana", "apple"];

const count = fruits.reduce((acc, fruit) => {
  acc[fruit] = (acc[fruit] || 0) + 1;
  return acc;
}, {});
console.log(count);
// { apple: 3, banana: 2, orange: 1 }

实例六:数组分组

const people = [
  { name: "张三", age: 25, department: "技术" },
  { name: "李四", age: 30, department: "市场" },
  { name: "王五", age: 28, department: "技术" },
  { name: "赵六", age: 35, department: "市场" },
];

const groupedByDept = people.reduce((acc, person) => {
  const dept = person.department;
  if (!acc[dept]) {
    acc[dept] = [];
  }
  acc[dept].push(person);
  return acc;
}, {});

console.log(groupedByDept);
/*
{
  技术: [
    { name: '张三', age: 25, department: '技术' },
    { name: '王五', age: 28, department: '技术' }
  ],
  市场: [
    { name: '李四', age: 30, department: '市场' },
    { name: '赵六', age: 35, department: '市场' }
  ]
}
*/

实例七:数组去重

const numbers = [1, 2, 2, 3, 4, 4, 5];

const unique = numbers.reduce((acc, current) => {
  if (!acc.includes(current)) {
    acc.push(current);
  }
  return acc;
}, []);
console.log(unique); // [1, 2, 3, 4, 5]

// 对象数组去重
const users = [
  { id: 1, name: "张三" },
  { id: 2, name: "李四" },
  { id: 1, name: "张三" },
  { id: 3, name: "王五" },
];

const uniqueUsers = users.reduce((acc, current) => {
  const exists = acc.find((user) => user.id === current.id);
  if (!exists) {
    acc.push(current);
  }
  return acc;
}, []);
console.log(uniqueUsers);
// [{ id: 1, name: '张三' }, { id: 2, name: '李四' }, { id: 3, name: '王五' }]

高阶用法

实例八:函数管道(Pipeline)

// 创建一个函数管道,从左到右执行函数
const pipe = (...functions) => {
  return (initialValue) => {
    return functions.reduce((acc, fn) => fn(acc), initialValue);
  };
};

// 使用示例
const add10 = (x) => x + 10;
const multiply2 = (x) => x * 2;
const subtract5 = (x) => x - 5;

const pipeline = pipe(add10, multiply2, subtract5);
console.log(pipeline(5)); // (5 + 10) * 2 - 5 = 25

实例九:实现 Promise 链式调用

const asyncOperations = [
  (x) => Promise.resolve(x + 1),
  (x) => Promise.resolve(x * 2),
  (x) => Promise.resolve(x + 3),
];

const runSequentially = (operations, initialValue) => {
  return operations.reduce(
    (promiseChain, operation) => promiseChain.then(operation),
    Promise.resolve(initialValue)
  );
};

// 使用
runSequentially(asyncOperations, 5).then((result) => console.log(result)); // 15: (5 + 1) * 2 + 3

实例十:深度合并对象

const deepMerge = (objects) => {
  return objects.reduce((merged, obj) => {
    Object.keys(obj).forEach((key) => {
      if (
        obj[key] &&
        typeof obj[key] === "object" &&
        !Array.isArray(obj[key])
      ) {
        merged[key] = deepMerge([merged[key] || {}, obj[key]]);
      } else {
        merged[key] = obj[key];
      }
    });
    return merged;
  }, {});
};

const obj1 = { a: 1, b: { c: 2, d: 3 } };
const obj2 = { b: { e: 4 }, f: 5 };
const obj3 = { b: { d: 6 }, g: 7 };

const result = deepMerge([obj1, obj2, obj3]);
console.log(result);
// { a: 1, b: { c: 2, d: 6, e: 4 }, f: 5, g: 7 }

实例十一:状态管理器(类似 Redux)

// 简单的状态管理实现
const createStore = (reducer, initialState) => {
  let state = initialState;
  const listeners = [];

  return {
    getState: () => state,
    dispatch: (action) => {
      state = reducer(state, action);
      listeners.forEach((listener) => listener());
    },
    subscribe: (listener) => {
      listeners.push(listener);
      return () => {
        const index = listeners.indexOf(listener);
        if (index > -1) {
          listeners.splice(index, 1);
        }
      };
    },
  };
};

// Reducer 使用 reduce 来处理动作队列
const todoReducer = (state = { todos: [] }, action) => {
  switch (action.type) {
    case "ADD_TODO":
      return {
        ...state,
        todos: [...state.todos, action.payload],
      };
    case "BATCH_ACTIONS":
      return action.actions.reduce(todoReducer, state);
    default:
      return state;
  }
};

const store = createStore(todoReducer, { todos: [] });

实际应用场景

场景一:Vue 项目中的数据处理

// 在 Vue 组件中处理 API 返回的数据
export default {
  data() {
    return {
      products: [],
      categoryStats: {},
    };
  },
  methods: {
    processProducts(products) {
      // 计算各分类的统计信息
      this.categoryStats = products.reduce((stats, product) => {
        const category = product.category;
        if (!stats[category]) {
          stats[category] = {
            count: 0,
            totalPrice: 0,
            avgPrice: 0,
          };
        }
        stats[category].count++;
        stats[category].totalPrice += product.price;
        stats[category].avgPrice =
          stats[category].totalPrice / stats[category].count;
        return stats;
      }, {});
    },
  },
};

场景二:表单验证

const validateForm = (formData, rules) => {
  return Object.keys(rules).reduce((errors, field) => {
    const fieldRules = rules[field];
    const fieldErrors = fieldRules.reduce((fieldErrors, rule) => {
      if (!rule.validator(formData[field])) {
        fieldErrors.push(rule.message);
      }
      return fieldErrors;
    }, []);

    if (fieldErrors.length > 0) {
      errors[field] = fieldErrors;
    }
    return errors;
  }, {});
};

// 使用示例
const formData = { email: "", password: "123" };
const rules = {
  email: [
    { validator: (val) => !!val, message: "邮箱不能为空" },
    { validator: (val) => /@/.test(val), message: "邮箱格式不正确" },
  ],
  password: [{ validator: (val) => val.length >= 6, message: "密码至少6位" }],
};

const errors = validateForm(formData, rules);
console.log(errors);

性能注意事项

  1. 避免在回调中创建新对象:每次迭代都创建新对象会影响性能

  2. 合理使用初始值:提供初始值可以避免类型错误

  3. 考虑使用其他方法:某些场景下 map、filter 等方法可能更直观

  4. 大数组处理:对于超大数组,考虑分批处理或使用 Web Workers

总结

reduce 是一个功能强大的数组方法,它能够:

  • 将数组转换为单一值

  • 实现复杂的数据转换

  • 替代多个链式调用

  • 实现函数式编程模式

  • 处理异步操作序列

最后更新时间:
贡献者: 何风顺
上一页
es6 模块导出方式全解析
下一页
forEach 中使用 async/await 的异步陷阱