likes
comments
collection
share

“巨坑”的toFixed

作者站长头像
站长
· 阅读数 6

在分享前,大家可以分享一下让你感到困惑的一些 api/运算符等。

  1. 双等号

“巨坑”的toFixed

“巨坑”的toFixed

  1. String.prototype.length / String.prototype.slice 等码元计数的

String 类型的 length 数据属性表示字符串的 UTF-16 码元长度。很多时候我们需要以码点来计数进行操作,大家感兴趣可以看我上次的分享《前端漫谈》

  1. Number.prototype.toFixed

我们今天就重点来研究 toFixed 这个api。

遇到的一些问题

  1. 他们的执行结果如何呢?

1.55.toFixed(1)

2.45.toFixed(1)

2.55.toFixed(1)

3.55.toFixed(1)

  1. 小数部分一样为啥 toFixed 的结果不一样?

1.55.toFixed(1) 和 2.55.toFixed(1)

  1. 0.1 + 0.2 !== 0.3 的真实原因

  2. 0.2 + 0.3 === 0.5 为什么是对的呢?

  3. 是不是所有小数都有会被截断存储呢?

  4. 小数截断时四舍五入合理吗🤔️?

“巨坑”的toFixed

今天就从小数的存储、运算、展示三个方面来分析下。

小数的存储

回顾一下整数和小数如何转二进制

整数转二进制

除二取余并倒排

“巨坑”的toFixed

小数转二进制

乘二取整并顺排

“巨坑”的toFixed

二进制转十进制

“巨坑”的toFixed

案例1 0.1 和 0.2 的二进制存储/表示

0.2 二进制存的啥?
0.2.toString(2)
'0.001100110011001100110011001100110011001100110011001101'
'0.0011001100110011001100110011001100110011001100110011001100'
0.2 在二进制里面没法真实表示,会被转成无限循环小数 '0011'

0.2 截断过程
0.001100110011001100110011001100110011001100110011001100110011...
我们看红色标记的位本来是 0 后面的都应该截断,因为下一位是 1 ,所以这里截断的时候进位了变成了
0.001100110011001100110011001100110011001100110011001101
所以实际存储的值会比 0.2 大。
0.2.toPrecision(50)
'0.20000000000000001110223024625156540423631668090820'

0.1 二进制存的啥?
0.1.toString(2)
'0.0001100110011001100110011001100110011001100110011001101'
'0.000110011001100110011001100110011001100110011001100110011'
重复上面的分析过程,得到实际存储的值会比 0.1 大。

案例2 小数部分一样为啥 toFixed 的结果不一样?🤔️🤔️

“巨坑”的toFixed

是不是所有小数都有会被截断存储呢?

“巨坑”的toFixed

能用二进制表示的场景

“巨坑”的toFixed

实际上 1/2^n 的小数是可以被有限二进制表示和存储的。

This content is only supported in a Feishu Docs

小数的运算

案例1 0.1 + 0.2 !== 0.3

0.3 二进制存的啥?
0.3.toString(2)
'0.010011001100110011001100110011001100110011001100110011'
0.010011001100110011001100110011001100110011001100110011
0.0100110011001100110011001100110011001100110011001100110011
重复上面的分析过程,得到实际存储的值会比 0.3 小。

0.1 + 0.2 > 0.3

通过分析,我们知道 0.3 实际存储的结果要比自身小。结合第一节中的案例1 0.1 和 0.2 的二进制存储,小数的存储中 0.2 和 0.1 实际存储的结果都比自身大,所以 0.1 + 0.2 执行结果肯定大于 0.3。

“巨坑”的toFixed

那 0.2 + 0.3 === 0.5 也是碰巧成立的。

“巨坑”的toFixed

“巨坑”的toFixed

小数的展示

浏览器一直在猜我们要做什么。0.2 实际存储是 0.20000000000000001110 ,但真正展示时 0.2。

“巨坑”的toFixed

0.3 - 0.2 为 0.09999999999999998,精度损失被放大了,浏览器也不敢截断了。

存储的0.3比真实的0.3小,存储的0.2比真实的0.2大,所以误差放大了。

“巨坑”的toFixed

回归本质

Number 编码(MDN)

JavaScript 的 Number 类型是一个双精度 64 位二进制格式 IEEE 754 值,类似于 Java 或者 C# 中的 double。这意味着它可以表示小数值,但是存储的数字的大小和精度有一些限制。简而言之,IEEE 754 双精度浮点数使用 64 位来表示 3 个部分:

  • 1 位用于表示符号(sign) (正数或者负数)
  • 11 位用于表示指数(exponent) (-1022 到 1023)
  • 52 位用于表示尾数(mantissa) (表示 0 和 1 之间的数值)

尾数(也称为有效数)是表示实际值(有效数字)的数值部分。指数是尾数应乘以的 2 的幂次。将其视为科学计数法:

“巨坑”的toFixed

尾数使用 52 比特存储,在二进制小数中解释为 1.… 之后的数字。因此,尾数的精度是 2(-52)(可以通过 Number.EPSILON 获得),或者十进制数小数点后大约 15 到 17 位;超过这个精度的算术会受到舍入的影响。

  • 一个数值可以容纳的最大值是 2(1024) 1(指数为 1023,尾数为基于二进制的 0.1111…),可以通过 Number.MAX_VALUE 获得。超过这个值的数会被替换为特殊的数值常量 Infinity

  • 只有在 -2(53) 1 到 2(53) 1 范围内(闭区间)的整数才能在不丢失精度的情况下被表示(可通过 Number.MIN_SAFE_INTEGERNumber.MAX_SAFE_INTEGER 获得),因为尾数只能容纳 53 位(包括前导 1)。

Number.EPSILON

Number.EPSILON === Math.pow(2, -52)

只要误差比 Number.EPSILON 小就认为是合理的误差阈值。Number.EPSILON === Math.pow(2, -52)

“巨坑”的toFixed

Number.MAX_SAFE_INTEGER

Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1

表示在 JavaScript 中最大的安全整数(2^53 – 1)

“巨坑”的toFixed

IEEE754

该标准的全称为IEEE二进制 浮点 数算术标准( ANSI /IEEE Std 754-1985)

baseconvert.com/ieee-754-fl…

www.h-schmidt.net/FloatConver…

IEEE 754 半精度浮点数:16 位符号 1 位,指数 5 位,尾数 10 位
IEEE 754 单精度浮点数:32 位符号 1 位,指数 8 位,尾数 23 位
IEEE 754 双精度浮点数:64 位符号 1 位,指数 11 位,尾数 52 位

“巨坑”的toFixed

解答案例2 小数一样为啥 toFixed 的结果不一样?🤔️🤔️

“巨坑”的toFixed

1.55 和 3.55 虽然他们小数部分完全一样,但是在 ieee754 中存储时,小数截断的位置不同导致,1.55 进位而3.55舍去了。

“巨坑”的toFixed

数值修约

数值修约是指在运算数字前,按照一定的规则确定一致的位数,然后舍去某些数字后面多余尾数的过程。

现在广泛使用的数值修约规则,主要有四舍五入五舍六入四舍六入五留双、无条件舍去规则。

四舍五入真的合理吗🤔️?

无条件修约

无条件修约分为下取整、上取整、截尾取整(无条件舍去)、无条件进位,分述如下:

“巨坑”的toFixed

四舍五入”方法看似合理,其实不然。 舍的是“ 1、2、3、4”,入的却是“5、6、7、8、9”,入的概率大于舍的概率。

0.0

0.1

0.2

银行家舍入法是由IEEE 754标准规定的浮点数取整算法,大部分的编程软件都使用的是这种方法。 所谓银行家舍入法,其实质是一种四舍六入五取偶(又称四舍六入五留双)法。

“四舍六入五成双”,也即“4舍6入5凑偶”,这里“四”是指≤4时舍去,"六"是指≥6时进上。"五"指的是根据5后面的数字来定,当5后有数时,舍5入1;当5后无有效数字时,需要分两种情况来讲:5前为奇数,舍5入1;5前为偶数,舍5不进(0是偶数)。

“巨坑”的toFixed

银行家舍入法来源,起初是为了解决银行结算时避免误差被放大而提出的。

就想要四舍五入怎么办?

一个的任意精度十进制类型JavaScript库: decimal.js

let a = 0.2, b = 0.1;
let c = new Decimal(a).add(new Decimal(b));
c // 0.3

四舍五入的效果

Math.round() 函数返回一个数字四舍五入后最接近的整数。

转成字符串,先放大后缩小。

还是直接上 lodash 吧!!!

_.round(2.55, 1)

“巨坑”的toFixed

IEEE-854

另外一个标准是IEEE 854,“与基数无关的浮点数”的“IEEE 854-1987标准”,有规定基数为2跟10的状况。但没有规定详细格式。所以非常少被採用。另外,从2000年開始,IEEE 754開始修订,被称为IEEE 754R(754r.ucbtest.org/)。目的是融合IEEE 754和IEEE 854标准。

IEEE 754-1985 和 IEEE 854-1987 均于 2008 年被 IEEE 754-2008 取代。

参考文献(图片均来源网络)

zhuanlan.zhihu.com/p/159127499

tc39.es/ecma262/#se…

developer.mozilla.org/zh-CN/docs/…

zh.wikipedia.org/wiki/%E6%95…

zh.wikipedia.org/wiki/IEEE_7…