一、前言
在前端开发中,保留两位小数 是非常常见的需求(比如金额、利率、百分比等)。看似简单,但 JS 的浮点数精度问题会带来一些坑。本篇汇总几种常用做法、优缺点和推荐场景,帮你快速选对方案。
二、常见方法与示例
1. 最简单:toFixed(2)(用于展示)
- 返回字符串(用于 UI 展示最方便)。
- 直接四舍五入,语法最简洁。
let n = 3.14159;
console.log(n.toFixed(2)); // "3.14"
console.log((3.1).toFixed(2)); // "3.10"
注意:toFixed 返回的是字符串,且会受到浮点精度影响:
console.log((1.005).toFixed(2)); // "1.00"(很多人期望 "1.01")
2. 推荐用于计算:基于放大 + 四舍五入(加 Number.EPSILON 修正)
- 适合需要数值类型的场景(参与后续计算)。
- 用
Number.EPSILON 修复常见的浮点误差。
function roundToTwo(num) {
return Math.round((num + Number.EPSILON) * 100) / 100;
}
console.log(roundToTwo(3.14159)); // 3.14
console.log(roundToTwo(1.005)); // 1.01
原理:先乘以 100 放大,四舍五入,最后除回去。Number.EPSILON 用于抵消二进制表示引入的微小误差。
- 适合格式化输出(含千分位、货币符号等)。
- 可控制最小/最大小数位。
const fmt = new Intl.NumberFormat('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
console.log(fmt.format(3.1)); // "3.10"
console.log(fmt.format(12345.678)); // "12,345.68"
4. 截断(不四舍五入):直接截断两位小数
function truncateTwo(num) {
return Math.trunc(num * 100) / 100;
// 或者 Math.floor(num * 100) / 100 (对负数行为不同)
}
console.log(truncateTwo(3.149)); // 3.14
console.log(truncateTwo(3.999)); // 3.99
5. 金额推荐做法:以“分”为单位(整数)计算
- 对于金钱计算,推荐把金额 *100 后作为整数处理,避免浮点累积误差,最后再格式化展示。
// 存储和计算都用整数 cents
let priceCents = Math.round(19.99 * 100); // 1999
let qty = 3;
let totalCents = priceCents * qty; // 5997
let total = totalCents / 100; // 59.97
6. 使用高精度库(需要极高精度或复杂业务)
- 推荐库:
decimal.js、big.js、bignumber.js 等。
- 适合金融级别精度或需要精确的小数运算场景。
示例(伪代码,需安装库):
// 使用 decimal.js(需 npm install decimal.js)
const Decimal = require('decimal.js');
let a = new Decimal(1.005);
console.log(a.toFixed(2)); // "1.01"
一、前言
在前端开发中,保留两位小数 是非常常见的需求(比如金额、利率、百分比等)。看似简单,但 JS 的浮点数精度问题会带来一些坑。本篇汇总几种常用做法、优缺点和推荐场景,帮你快速选对方案。
二、常见方法与示例
1. 最简单:toFixed(2)(用于展示)
- 返回字符串(用于 UI 展示最方便)。
- 直接四舍五入,语法最简洁。
let n = 3.14159;
console.log(n.toFixed(2)); // "3.14"
console.log((3.1).toFixed(2)); // "3.10"
注意:toFixed 返回的是字符串,且会受到浮点精度影响:
console.log((1.005).toFixed(2)); // "1.00"(很多人期望 "1.01")
2. 推荐用于计算:基于放大 + 四舍五入(加 Number.EPSILON 修正)
- 适合需要数值类型的场景(参与后续计算)。
- 用
Number.EPSILON 修复常见的浮点误差。
function roundToTwo(num) {
return Math.round((num + Number.EPSILON) * 100) / 100;
}
console.log(roundToTwo(3.14159)); // 3.14
console.log(roundToTwo(1.005)); // 1.01
原理:先乘以 100 放大,四舍五入,最后除回去。Number.EPSILON 用于抵消二进制表示引入的微小误差。
- 适合格式化输出(含千分位、货币符号等)。
- 可控制最小/最大小数位。
const fmt = new Intl.NumberFormat('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
console.log(fmt.format(3.1)); // "3.10"
console.log(fmt.format(12345.678)); // "12,345.68"
4. 截断(不四舍五入):直接截断两位小数
function truncateTwo(num) {
return Math.trunc(num * 100) / 100;
// 或者 Math.floor(num * 100) / 100 (对负数行为不同)
}
console.log(truncateTwo(3.149)); // 3.14
console.log(truncateTwo(3.999)); // 3.99
5. 金额推荐做法:以“分”为单位(整数)计算
- 对于金钱计算,推荐把金额 *100 后作为整数处理,避免浮点累积误差,最后再格式化展示。
// 存储和计算都用整数 cents
let priceCents = Math.round(19.99 * 100); // 1999
let qty = 3;
let totalCents = priceCents * qty; // 5997
let total = totalCents / 100; // 59.97
6. 使用高精度库(需要极高精度或复杂业务)
- 推荐库:
decimal.js、big.js、bignumber.js 等。
- 适合金融级别精度或需要精确的小数运算场景。
示例(伪代码,需安装库):
// 使用 decimal.js(需 npm install decimal.js)
const Decimal = require('decimal.js');
let a = new Decimal(1.005);
console.log(a.toFixed(2)); // "1.01"
三、为什么 (1.005).toFixed(2) 得到 "1.00"?
简述原因:JS 使用 IEEE-754 二进制浮点数表示,1.005 在二进制下并不是精确的 1.005,而是一个略小于 1.005 的数(例如 1.004999999…)。toFixed 的四舍五入是在该近似值上进行的,因此得到 "1.00"。使用 Number.EPSILON 或高精度库可以规避这种问题。
四、对比表(简洁)
| 场景 |
方法 |
类型 |
是否四舍五入 |
备注 |
| 简单展示 |
toFixed(2) |
字符串 |
是 |
语法简单,但受浮点影响 |
| 精确计算 |
Math.round((n+EPS)*100)/100 |
Number |
是 |
推荐用于普通计算 |
| 国际化展示 |
Intl.NumberFormat |
字符串 |
是 |
支持千分位、货币 |
| 不四舍五入 |
Math.trunc/Math.floor |
Number |
否 |
业务需截断时用 |
| 金额高精度 |
整数(分) + 整数运算 |
Number(整数) |
— |
推荐用于金融计算 |
| 极高精度 |
decimal.js 等库 |
BigDecimal |
是 |
复杂金融场景使用 |
五、实用函数集合(可直接复制)
// 1. 返回 Number(四舍五入到两位)
function roundToTwo(num) {
return Math.round((num + Number.EPSILON) * 100) / 100;
}
// 2. 返回 String(两位小数,适合展示)
function toFixedString(num) {
return (Math.round((num + Number.EPSILON) * 100) / 100).toFixed(2);
}
// 3. 截断(不四舍五入)
function truncateTwo(num) {
return Math.trunc(num * 100) / 100;
}
// 4. Intl 格式化(展示,带千分位)
function formatTwoIntl(num, locale = 'zh-CN') {
return new Intl.NumberFormat(locale, {
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(num);
}
// 用例
console.log(roundToTwo(1.005)); // 1.01
console.log(toFixedString(3.1)); // "3.10"
console.log(truncateTwo(3.199)); // 3.19
console.log(formatTwoIntl(12345.6)); // "12,345.60"
六、实战建议(总结)
- 仅展示数据:
toFixed(2) 或 Intl.NumberFormat(推荐国际化格式化)。
- 参与计算:不要直接用
toFixed 的字符串再转数字,优先用放大 + Math.round(配合 Number.EPSILON)。
- 金额运算:优先把金额转为“分”(整数)统一计算,最后再格式化。
- 对精度要求极高:引入专门的高精度库(
decimal.js、big.js)。
- 避免盲目使用
toFixed 做运算:toFixed 返回字符串,参与后续数学运算需谨慎转换。
七、结束语
保留两位小数是前端最基础的事,但要分清「展示层」和「计算层」。按场景选方法:展示用格式化,计算用整数或修正过的四舍五入,金融场景用高精度库或整数(分)策略。