侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 104 篇文章
  • 累计收到 7 条评论

toFixed 舍的是二进制值,Intl 舍的是最短十进制写法

2026-9-22 / 0 评论 / 18 阅读

toFixed 舍的是二进制值,Intl 舍的是最短十进制写法

商品页要显示「1.005 元」这类价格,保留两位小数,接口原样给了这个数,页面上却出现 1.00。单价栏和总价栏用的还是不同写法,一栏用 toFixed(2),另一栏用 Intl.NumberFormat,两个数字对不上,客诉就这么来的。

七个数,三种写法,落在三个地方

脚本 tools/verify-money-rounding.mjs,Node v26.8.1,Intl.NumberFormat('zh-CN', {minimumFractionDigits: 2, maximumFractionDigits: 2})

输入 toFixed(2) Math.round(x*100)/100 Intl 两位
1.005 1.00 1 1.01
2.675 2.67 2.68 2.68
8.575 8.57 8.57 8.58
1.45 1.45 1.45 1.45
1.015 1.01 1.01 1.02
1234.565 1234.57 1234.57 1,234.57
0.1 + 0.2 0.30 0.3 0.30

四行里 toFixedIntl 给出不同答案。Math.round(x*100)/100 的落点又是第三种:1.005 给 1,因为中间那次乘法把 100.49999999999999 交给了 Math.round。最后一行还多一个差别,Intl 默认带千分位,toFixed 不带,拼字符串时得留意这一层。

差在舍入之前取了哪个值

把这三个数用二十位精度打出来,答案就摆在眼前:1.005 在 double 里的精确值是 1.0049999999999998934,2.675 是 2.6749999999999998224。

toFixed 的输入是二进制精确值,这个值比 1.005 小,往两位上舍,落到 1.00。Intl 先把它转成最短的十进制写法,也就是「1.005」,再按十进制规则处理平局,于是给 1.01。2.675 的精确值比它短写更小,Intl 仍然按十进制平局往上走,给 2.68。

Math.round(x*100)/100 则是另一条路:乘法本身发生在二进制里,1.005 乘 100 得到 100.49999999999999,取整自然偏低。三种写法没有一种是「错的」,它们只是各自忠实于不同的那一层。

roundingMode 能选,但选不了「按十进制」

Node 的 Intl 支持 roundingMode 参数。同一批数字,六档规则给出的结果如下。

roundingMode 2.5 3.5 -2.5 1.005
halfExpand 3 4 -3 1.01
halfEven 2 4 -2 1
halfTrunc 2 3 -2 1
ceil 3 4 -2 1.01
floor 2 3 -3 1
trunc 2 3 -2 1

halfEven 是银行家舍入,2.5 落到 2、3.5 落到 4。要注意的是这里的平局判断也发生在二进制值上,所以换规则能改行为,换不出「照十进制字面来舍」这件事。1.005 在 halfEven 下给 1,在 halfExpand 下给 1.01,两种都站得住,取决于业务想要哪一种。

二十万个随机数里只有一个不一致

有人会说,换 Intl 总比 toFixed 稳。把两个 API 放在同一批数据上比一比。

=== 6. 同样的输入,Intl 与 toFixed 完全一致(说明换 API 不解决问题)===
   20 万个随机数:一致 199999 个,不一致 1 个

二十万个随机浮点数里只有一个不一致,看不出来是因为随机数撞上平局的概率低。价格数据不一样,1.005、2.675、8.575 这些都是人工定的零售价,恰好是平局高发区,越靠近定价规则越容易踩到。前端这一层调 API、加参数,都改不掉根因:钱不该用 double 存。

整数分是唯一稳的那条路

先转成分再做运算,最后自己拼字符串。

=== 5. 整数分方案:先转成分,再自己拼字符串 ===
   10.05 → 1005 分 → 10.05
   1.005 → 100 分 → 1.00
   2.675 → 267 分 → 2.67
   0.30000000000000004 → 30 分 → 0.30
   99999999999.99 → 9999999999999 分 → 99999999999.99
   注意第一栏:1.005 走 toFixed 就已经变成 100 分,错误在进入 BigInt 之前就发生了

这段输出里最该看的是第一栏。整数分方案本身没问题,但只要入口那一步是拿 toFixed(2) 把浮点转成分,错误在转换时就发生了,后面用 BigInt 再稳也白搭。入口如果是接口给的字符串或者分单位的整数,直接 BigInt 接住,这条路才成立;浮点只在最后展示时才碰。我在本机试过的顺序就是这样,先定单位,再谈 API。

至于展示层的取舍:如果价格只用于显示、参与计算的值来自整数分,那 toFixed(2)Intl 用哪个都行,Intl 多了千分位和本地化,代价是要记得它会加逗号。反过来,只要页面上算的是钱本身,这两个都别当依据。

    🤞 分享