🔍 十二、reduce() 方法:将数组元素累积计算为单个值
reduce() 方法对数组中的每个元素按序执行一个由您提供的 reducer 函数,每一次运行会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值。
简单来说,它的核心思想是 "累积":将数组"压缩"成一个值(这个值可以是数字、对象、数组,甚至是另一个函数)。
1. 基本语法
javascript
arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])
参数解析:
- callback (必须):执行数组中每个元素的函数。
accumulator(acc):累积器 。它是上一次回调函数的返回值,或者是初始值initialValue。这是reduce的核心。currentValue(cur):当前正在处理的元素。index(可选):当前元素的索引。array(可选):原数组。
- initialValue (强烈建议):作为第一次调用
callback时的accumulator的初始值。
**返回值:**累积处理后的最终结果。
2. 基础示例
(1) 数值累加(最经典示例)
计算数组中所有数字的总和。
javascript
const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((acc, cur) => {
return acc + cur;
}, 0); // 初始值设为 0
console.log(sum); // 输出: 10
执行过程解析:
- 第1次:
acc= 0 (初始值),cur= 1 -> 返回 1。 - 第2次:
acc= 1,cur= 2 -> 返回 3。 - 第3次:
acc= 3,cur= 3 -> 返回 6。 - 第4次:
acc= 6,cur= 4 -> 返回 10。
(2) 求数组最大值
javascript
const arr = [10, 5, 100, 2];
const max = arr.reduce((acc, cur) => {
return Math.max(acc, cur);
}, arr[0]); // 初始值设为第一个元素
console.log(max); // 输出: 100
3. 企业开发中的实际应用场景
reduce 的强大在于它的返回值可以是任意类型,这使得它能处理非常复杂的业务逻辑。
场景 1:数组转对象
在后端接口对接中,有时需要将数组转换为以 ID 为 Key 的对象,以便快速查找。
javascript
const users = [
{ id: 101, name: 'Alice' },
{ id: 102, name: 'Bob' }
];
// 目标: { 101: { id: 101, name: 'Alice' }, 102: { ... } }
const userMap = users.reduce((acc, user) => {
acc[user.id] = user; // 以 id 为键,对象为值
return acc; // 必须返回 acc
}, {});
console.log(userMap[101].name); // 'Alice' (查找速度 O(1))
场景 2:数组扁平化
将多维数组转化为一维数组(虽然现在有 flat(),但 reduce 实现能体现逻辑)。
javascript
const nestedArr = [[1, 2], [3, 4], [5]];
const flatArr = nestedArr.reduce((acc, cur) => {
return acc.concat(cur);
}, []);
console.log(flatArr); // 输出: [1, 2, 3, 4, 5]
场景 3:数据统计与分组
将数组中的元素按类别分组,例如按角色分组用户。
javascript
const people = [
{ name: 'Alice', role: 'admin' },
{ name: 'Bob', role: 'user' },
{ name: 'Charlie', role: 'admin' }
];
const groupByRole = people.reduce((acc, person) => {
const key = person.role;
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(person);
return acc;
}, {});
console.log(groupByRole);
// 输出:
// {
// admin: [{ name: 'Alice',... }, { name: 'Charlie',... }],
// user: [{ name: 'Bob',... }]
// }
场景 4:函数式编程管道
在复杂的数据处理流程中,组合多个函数。
javascript
const process = (input) => input
.split('')
.reverse()
.join('');
// 或者使用 reduce 组合函数
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
4. 注意事项与常见陷阱
reduce 虽然强大,但也是最容易出错的数组方法之一。
(1) 千万不要忘记 return
这是新手最容易犯的错误。reducer 函数必须返回 新的累积值(accumulator)。如果你忘记写 return,下一次迭代的 accumulator 就会变成 undefined。
错误示范:
javascript
const sum = arr.reduce((acc, cur) => {
acc + cur; // 忘记 return,结果将是 NaN
}, 0);
(2) 初始值 initialValue 的重要性
虽然 reduce 允许不写初始值(此时第一次迭代的 acc 是数组第一个元素,cur 是第二个元素),但这极易引发 Bug。
- 空数组报错 :如果数组为空且没有提供初始值,
reduce会抛出TypeError。 - 逻辑混乱 :如果不提供初始值,索引
index会从 1 开始,容易造成困惑。
最佳实践: 始终为 reduce 提供第二个参数(初始值),哪怕它是 0、{} 或 []。
(3) 纯函数与引用陷阱
如果你累积器是对象或数组,请注意不要直接修改累积器的引用(除非你很清楚自己在做什么),这可能会带来副作用。但在性能优化场景下,原地修改累积器对象(如 acc[key] = val)是可以接受的,因为它比深拷贝更高效。
(4) 可读性权衡
不要为了用 reduce 而用 reduce。如果 map、filter 或 forEach 能更直观地解决问题,就不要强行使用 reduce。过度复杂的 reduce 会让代码变得晦涩难懂,维护成本增加。
🔍 十三、toFixed() 方法:保留小数位
toFixed() 是 Number 对象的一个原型方法。它使用定点表示法来格式化一个数值,把数字转换为字符串,并保留指定位数的小数。
核心作用:
- 格式化数字的小数位数。
- 自动进行四舍五入(注意:并非标准的四舍五入,详见后文)。
- 返回字符串。
1. 基本语法
javascript
numObj.toFixed(digits)
参数解析:
- digits (可选):小数点后显示的位数。取值范围是 0 到 20(包括 0 和 20)。
- 如果省略,默认为 0。
- 如果超出范围,部分环境可能支持更大范围,也可能抛出
RangeError。
返回值:
- 返回一个字符串,代表该数字保留指定小数位后的形式。
2. 基础示例
(1) 基础保留小数
javascript
const num = 3.14159;
console.log(num.toFixed(2)); // 输出: "3.14"
console.log(num.toFixed(0)); // 输出: "3"
console.log(num.toFixed(4)); // 输出: "3.1416" (末尾补 0)
(2) 数据类型陷阱
这是新手最容易踩的坑:结果是字符串,不是数字。
javascript
const num = 10.5;
const result = num.toFixed(2);
console.log(result); // 输出: "10.50"
console.log(typeof result); // 输出: "string"
// 如果想变回数字进行计算,需要转换
console.log(Number(result)); // 输出: 10.5
3. 企业开发中的实际应用场景
场景 1:电商价格展示(补零)
后端返回的价格可能是整数 100 或两位小数 99.9,前端展示时通常要求统一格式 100.00 或 99.90。
javascript
const price = 99.9;
const displayPrice = price.toFixed(2);
// 模板中直接使用
console.log(`¥${displayPrice}`); // 输出: ¥99.90
场景 2:数据可视化图表标签
在使用 ECharts 等图表库时,tooltip 或 y 轴标签通常要求保留两位小数,避免过长的小数挤占空间。
javascript
const data = [12.345, 67.891, 45.001];
const formattedData = data.map(item => item.toFixed(2));
// 输出: ["12.35", "67.89", "45.00"]
场景 3:输入框金额格式化
在"金额输入框"失去焦点时,自动格式化为标准金额格式。
javascript
const handleBlur = (e) => {
const value = parseFloat(e.target.value);
if (!isNaN(value)) {
form.price = value.toFixed(2);
}
};
4. 注意事项与"银行家舍入"陷阱
这是关于 toFixed() 最重要的部分,也是产生"莫名其妙" Bug 的根源。
(1) "四舍六入五成双"问题
toFixed() 使用的并不是我们小学数学里的"四舍五入",而是银行家舍入法。
简单规则如下:
- < 5:舍去。
- > 5:进位。
- = 5 :分两种情况。
- 如果 5 后面还有非 0 数字,进位。
- 如果 5 后面没有数字或全是 0,则看 5 前一位:奇数进位,偶数舍去。
诡异示例:
javascript
(1.35).toFixed(1); // 输出: "1.4" (5前面是3,奇数,进位)
(1.45).toFixed(1); // 输出: "1.4" (5前面是4,偶数,舍去!预期可能是1.5)
(2.55).toFixed(1); // 输出: "2.5" (5前面是5,奇数,理应进位,但浮点数精度导致结果为2.5)
(2.555).toFixed(2); // 输出: "2.55" (有时候浮点数精度会干扰这个判断)
解决方案: 如果你必须严格遵循"四舍五入",不要直接依赖 toFixed。可以使用 Math.round() 配合运算:
javascript
// 封装一个严格的四舍五入函数
function roundFixed(num, decimals) {
const factor = Math.pow(10, decimals);
const temp = num * factor * 10; // *10是为了处理 .5 的情况
// 修正浮点数计算误差
const rounded = Math.round(temp) / 10 / factor;
return rounded.toFixed(decimals);
}
console.log(roundFixed(1.45, 1)); // 输出: "1.5"
或者使用第三方库如 decimal.js、lodash 来处理精度问题。
(2) 返回值是字符串
再次强调,toFixed() 返回字符串。如果你在计算总价时直接相加,会发生字符串拼接。
javascript
let total = 0;
const price = 10.5;
total += price.toFixed(2);
console.log(total); // 输出: "010.50" (字符串拼接!)
(3) 浮点数精度问题
由于 JavaScript 采用 IEEE 754 标准,浮点数运算本身就不精确。toFixed 有时能掩盖问题,有时会暴露问题。
javascript
(0.335).toFixed(2); // 输出: "0.33" (因为 0.335 在内存中可能比 0.335 小一点点)