blog
blog copied to clipboard
基础系列 - 奇怪的 0.1 + 0.2 与 IEEE 754
在计算机的世界中,浮点数的表示范围优先。浮点数只是可以近似的标识一个数而已。与许多其他编程语言不同,JavaScript 并未定义不同类型的数字数据类型,而是始终遵循国际 IEEE 754 标准,将数字存储为双精度浮点数。
- sign 符号位
- exponent 指数位
- mantissa 尾数部分
十进制转二进制补课(敲黑板)
整数部分
用2去除十进制整数,可以得到一个商和余数;再用2去除商,又会得到一个商和余数,如此进行,直到商为零时为止,然后把先得到的余数作为二进制数的低位有效位,后得到的余数作为二进制数的高位有效位,依次排列起来。
小数部分
用2乘十进制小数,可以得到积,将积的整数部分取出,再用2乘余下的小数 部分,又得到一个积,再将积的整数部分取出,如此进行,直到积中的小数部分为零,或者达到所要求的精度为止。然后把取出的整数部分按顺序排列起来。
动手试一下
接下来我们手动尝试一下10.3 + 12.4
这个组合,我先帮你在Chrome devtool测试了一下
整数部分
小数部分
整合
整数部分: 10
==> 1010
小数部分: 0.3
==> 01 0011 0011 0011 0011 ......
科学计数表示: 1.010010011001100110011... x 103
IEEE 表示法
-
符号位
- 0表示正数,1表示负数
- 这里是 0
-
指数部分
- 双精度浮点数这部分一共是11位,也就是基准偏移量是 211 - 1 - 1。科学计数法的幂值是3也就是偏移量为3。这里指数部分就是
基准偏移量 + 偏移量(可能为负喔)
。 - 1023 + 3 = 1026,转换为二进制就是
100 0000 0010
- 双精度浮点数这部分一共是11位,也就是基准偏移量是 211 - 1 - 1。科学计数法的幂值是3也就是偏移量为3。这里指数部分就是
-
尾数部分
- 尾数部分就直接将科学计数表示法的小数部分迁移过来就行,最多52位,不满的话就用0来补。
-
0100 1001 1001 1001 1001 1001 1001......
按照Double Float Precision IEEE 754
的格式拼起来就是0 100 0000 0010 0100 1001 1001 10001 1001 1001......
结果验证
加法运算
按照上面IEEE 双精度的表示法,两个数字分别如下,然后进行尾数求和
0 100 0000 0010 0100 1001 1001 1001 1001 1001...
0 100 0000 0010 1000 1001 1001 1001 1001 1001...
——————————————————————————————————————————————————
1101 0011 0011 0011 0011 0010
然后进行的是尾数的规格化: 1101 0011 0011 0011 0011 0010
===> 0110 1001 1001 1001 1001 1001
,其实就是把末尾的0
给去掉了,前面补0
。
表示范围
因为mantissa(尾数部分)
的固定长度为52位,最多可以表示252 + 1(也即是9007199254740992)个数字,用科学技术法来表示也就是9.007199254740992 x 10 16
在控制台我们可以测试到
(0.1).toPrecision(16) // "0.1000000000000000"
(0.1).toPrecision(17) // "0.10000000000000001"
(0.1).toPrecision(18) // "0.100000000000000006"
(0.1).toPrecision(20) // "0.10000000000000000555"
(0.2).toPrecision(16) // "0.2000000000000000"
(0.2).toPrecision(17) // "0.20000000000000001"
(0.2).toPrecision(18) // "0.200000000000000011"
(0.3).toPrecision(16) // "0.3000000000000000"
(0.3).toPrecision(17) // "0.29999999999999999"
(0.3).toPrecision(20) // "0.29999999999999998890"
所以,我们平时看到的 0.1
并不是只有 0.1
,看到的 0.3
也不一定够0.3
,只是显示精度作怪而已。
toPrecision 与 toFixed
// 以定点表示法或指数表示法表示的一个数值对象的字符串表示,四舍五入到 precision 参数指定的显示数字位数。
numObj.toPrecision(precision)
// 使用定点表示法表示给定数字的字符串。
numObj.toFixed(digits)
其实,precision
是指从小数点开始从左往右开始数,第一个不为0
的数字开始计数。而fixed
是指小数点后开始算的位数。
总结
显而易见,在IEEE体系中,二进制只能够近似地表示某个浮点数数值。比如0.3
,使用标准的转换方式,我们永远也达不到终止条件---小数位为0。在存储空间有限的情况下,无尽的循环到达边界时,就不得不进行类似十进制的四舍五入进位
了。
实战
在实际项目中,直接使用float
、double
进行金额数值运算也就有可能出现未知的情况,在许多语言中会有Decimal
数据类型(例如Python)。而在JavaScript中则是推荐使用Decimal.js