最近看到不少关于 toFixed 方法的舍入逻辑的文章,其中有不少文章认为 toFixed 的实现采用的是所谓的银行家算法,也就是四舍六入五成双。也有文章认为是四舍五入。也都给了不少例证。还有些人认为无法确定,主要看浏览器厂商是怎么实现的。为什么会出现这些讨论,主要还是因为这个方法的计算结果有时候真的是让人猝不及防。
toFixed 的问题
直接看几个执行结果感受一下
1 | 1.36.toFixed(1) // 1.4 没有问题 |
这种没有卡在 5 上面的都不会出什么问题,四舍六入很正常。问题在于中间这个5。看下面的例子
1 | 1.35.toFixed(1) // 1.4 这里的5入 |
有不少人看到这里就认为 toFixed 采用的是四舍六入五成双了,上面这两个结果完全的诠释了出现5的时候,如果前面的数字是奇数,则进1让其成为偶数。如果前面的数字是偶数,直接舍去5
再多看几个例子
1 | 1.15.toFixed(1) // 1.1 并没有进1 成1.2来凑偶数 |
那 toFixed 在遇到5的时候,到底用的什么逻辑呢,与其用不同的数字去执行 toFixed 查看结果,来验证自己的猜想,不如直接看看在 js 标准中,关于 toFixed 是怎么定义的
toFixed 的实现标准
上图是直接从 ECMAScript® 2024 Language Specification 文档中截的图,其中最主要的逻辑是11.a这一步
稍作解释如下:
x 是我们要操作的数,f 是保留的小数位数, n 是在这一步找出的结果,n需要满足的条件是, 整数且使表达式 $ n / 10 ^ f - x $ 最接近于0, 如果这样的数找到2个,取其中较大的那个
对于 1.05.toFixed(1)
这个表达式,x 是 1.05, f 是 1,把 数字代进表达式计算,这个 n 应该是最接近10.5的一个整数,这样的数字有两个,分别是 10 和 11,根据规则最较大的11, 经过后续 11.c 的一些操作,主要就是数值缩放,确定小数点位置。主要功能还是寻找 n 的过程。
通过toFixed的实现逻辑,可以看出来toFixed 采用的是四舍五入的逻辑,在它的规范里没有提到任何取偶数的概念
既然如此,那1.15.toFixed(1)
的结果应该是1.2才对,为什么实际执行结果却是1.1。其实看下面的代码应该就能感受一二了
1 | 1.15.toPrecision(100) |
1.15 在系统中就不是严格上的 1.15,通过这个高精度表示的数字来看,就知道为什么 1.15.toFixed(1) 的结果为什么是1.1了,因为小数点后第二位根本不是5而且4,所以直接舍去了。
在浏览器控制台执行下面 js 代码验证
1 | 11 / 10 - 1.15 // -0.04999999999999982 更接近于0, 所以取 11 |
疑问
如果你在浏览器控制台去执行下面代码
1 | 87.55.toFixed(1) // 87.5 |
和0的差距是一样的, 按逻辑 87.55.toFixed(1)
的结果应该是 87.6, 但是实际结果却是87.5。看看高精度表示的87.55可以理解
1 | 87.55.toPrecision(100) |
但是这里为什么计算出来的和0的距离为什么不像 1.15 的例子那样呢?
这里可能就涉及到开头说的浏览器在实现 js 引擎的时候用的是什么逻辑了(如果你知道可以评论区解释一下)。但不管浏览器具体采用的是什么逻辑,通过 js 标准以及这些例子,可以确定 toFixed 方法使用的逻辑是四舍五入,至于为什么部分数字不符合预期,是由于精度问题导致了。就好像为什么 0.1 + 0.2 !== 0.3
是一样的
解决方案
在明白问题原因之后,可以采用使用 yatter 中的 toFixed 方法来规避问题
1 | import {toFixed} from 'yatter'; |
或者采用 decimal-format ,提供了更丰富的格式化功能
1 | import DecimalFormat from 'decimal-format'; |
还有更多舍入模式,以及输出格式不一一举例说明