保留几位小数,这在前端来 format 是很常见的。但直接之用 JavaScript 里数值的 toFixed
函数是有坑的。甩两个 bad case 看看:
(1.005).toFixed(2
) 为 “1.00”,应该是 “1.01”
(4.05).toFixed(1)
为 “4.0”,应该是 “4.1”
结论就是:前端对敏感型数字的四舍五入展示,如价格、收入、跟金额有关的,千万不要直接 toFixed,以免被客户追究。
原理
JS 语言诟病,浮点数本身存在精度问题,举个例子:0.1 + 0.2 !== 0.3
那问题来了,保留2位小数,能否先乘 100,然后四舍五入取整后再除 100 呢?
不妨试下 Math.round(1.005 * 100) / 100
结果也不对,因为 1.005 * 100
的结果是 100.49999999999999
。而 0.1 + 0.2
的结果是 0.30000000000000004
,这就很迷了,什么时候会比正确值略大,什么时候又会略小,看不到规律。
其实这要跟浮点数的二进制存储有关了,计算机是无法精确地表示任意浮点数的。有的数能用 (1/2)^n 的相加表示的话,那这个浮点数就是精确的,比如 0.5, 0.75, 0.875。否则由于浮点数存储有位数限制,肯定是不精确的。
JS 里的 number 类型只有 16 位精度,所以浮点数运算捉襟见肘。
另一方面,JS 对超长的整数也表示不了,比如很多系统里的订单号,在后端会用 Long 来表示,数字超过 16 位时在 JS 里就没法精确表示了。30001519369635544130
这个大数字末几位就会被抹成零,就会导致前端看到的订单号与后端实际单号不一致。所以超过 16 位的大数字,一定要求后端以 String 来给到前端。
实现
思路是,如果保留2位小数,那先乘 100 后四舍五入取整,然后把小数点加在倒数第 2 位之前。
1 | import * as MAX_SAFE_INTEGER from 'core-js/library/fn/number/max-safe-integer'; |
测试用例
1 | import test from 'ava'; |
最后,在前端 JS 里最好别做数值计算,如果一定要计算,可以使用 number-precision 库。
还有,价格这种字段,无论如何都不要在前端计算!做一个有底线的前端。