在分享前,大家可以分享一下让你感到困惑的一些 api/运算符等。
- 双等号
- String.prototype.length / String.prototype.slice 等码元计数的
String
类型的 length
数据属性表示字符串的 UTF-16 码元长度。很多时候我们需要以码点来计数进行操作,大家感兴趣可以看我上次的分享《前端漫谈》。
- Number.prototype.toFixed
我们今天就重点来研究 toFixed 这个api。
遇到的一些问题
- 他们的执行结果如何呢?
1.55.toFixed(1)
2.45.toFixed(1)
2.55.toFixed(1)
3.55.toFixed(1)
- 小数部分一样为啥 toFixed 的结果不一样?
1.55.toFixed(1) 和 2.55.toFixed(1)
-
0.1 + 0.2 !== 0.3 的真实原因
-
0.2 + 0.3 === 0.5 为什么是对的呢?
-
是不是所有小数都有会被截断存储呢?
-
小数截断时四舍五入合理吗🤔️?
今天就从小数的存储、运算、展示三个方面来分析下。
小数的存储
回顾一下整数和小数如何转二进制
整数转二进制
除二取余并倒排
小数转二进制
乘二取整并顺排
二进制转十进制
案例1 0.1 和 0.2 的二进制存储/表示
arduino
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 的结果不一样?🤔️🤔️
是不是所有小数都有会被截断存储呢?
能用二进制表示的场景
实际上 1/2^n 的小数是可以被有限二进制表示和存储的。
This content is only supported in a Feishu Docs
小数的运算
案例1 0.1 + 0.2 !== 0.3
vbscript
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。
那 0.2 + 0.3 === 0.5 也是碰巧成立的。
小数的展示
浏览器一直在猜我们要做什么。0.2 实际存储是 0.20000000000000001110 ,但真正展示时 0.2。
0.3 - 0.2 为 0.09999999999999998,精度损失被放大了,浏览器也不敢截断了。
存储的0.3比真实的0.3小,存储的0.2比真实的0.2大,所以误差放大了。
回归本质
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 的幂次。将其视为科学计数法:
尾数使用 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_INTEGER
和Number.MAX_SAFE_INTEGER
获得),因为尾数只能容纳 53 位(包括前导 1)。
Number.EPSILON
Number.EPSILON === Math.pow(2, -52)
只要误差比 Number.EPSILON
小就认为是合理的误差阈值。Number.EPSILON === Math.pow(2, -52)
Number.MAX_SAFE_INTEGER
Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1
表示在 JavaScript 中最大的安全整数(2^53 -- 1)
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 位 |
解答案例2 小数一样为啥 toFixed 的结果不一样?🤔️🤔️
1.55 和 3.55 虽然他们小数部分完全一样,但是在 ieee754 中存储时,小数截断的位置不同导致,1.55 进位而3.55舍去了。
数值修约
数值修约是指在运算数字前,按照一定的规则确定一致的位数,然后舍去某些数字后面多余尾数的过程。
现在广泛使用的数值修约规则,主要有四舍五入 、五舍六入 和四舍六入五留双、无条件舍去规则。
四舍五入真的合理吗🤔️?
无条件修约
无条件修约分为下取整、上取整、截尾取整(无条件舍去)、无条件进位,分述如下:
"四舍五入"方法看似合理,其实不然。 舍的是" 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是偶数)。
银行家舍入法来源,起初是为了解决银行结算时避免误差被放大而提出的。
就想要四舍五入怎么办?
一个的任意精度十进制类型JavaScript库: decimal.js
csharp
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)
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
developer.mozilla.org/zh-CN/docs/...