多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
[TOC] # 两个既定的事实 1. 在JS中能否表示的数字的绝对值范围是5e-324 ~ 1.7976931348623157e+308,这一点可以通过`Number.MAX_VALUE`和`Number.MIN_VALUE`来得到证实 2. 在JS中能够表示的最大安全整数的范围是:-9007199254740991 ~ 9007199254740991,这一点可以通过`Number.MIN_SAFE_INTEGER`和`Number.MAX_SAFE_INTEGER`来求证 # 两个存在的问题 1. 在四则运算中存在精度丢失的问题,比如: `01 + 0.2 //0.30000000000000004` 2. 超过最大安全整数的运算是不安全的,比如:`9007199254740991 + 2 // 9007199254740992` # 存储 1. 把这个浮点数转成对应的二进制数,并用科学计数法表示 2. 把这个数值通过[IEEE 754](https://link.juejin.im?target=https%3A%2F%2Fzh.wikipedia.org%2Fwiki%2FIEEE_754)标准表示成真正会在计算机中存储的值 我们知道,JS中的Number类型使用的是双精度浮点型,也就是其他语言中的double类型。而双精度浮点数使用64 bit来进行存储,结构图如下: ![](https://user-gold-cdn.xitu.io/2018/1/30/16144bd12f9b3376?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 也就是说一个Number类型的数字在内存中会被表示成:`s x m x 2^e`这样的格式。 在[ES规范](https://link.juejin.im?target=http%3A%2F%2Fes5.github.io%2F%23x8.5)中规定e的范围在-1074 ~ 971,而m最大能表示的最大数是52个1,最小能表示的是1,这里需要注意: 二进制的第一位有效数字必定是1,因此这个1不会被存储,可以节省一个存储位,因此尾数部分可以存储的范围是1 ~ 2^(52+1) **也就是说Number能表示的最大数字绝对值范围是 2^-1074 ~ 2^(53+971)** # 精度丢失 前面提到,计算机中存储小数是先转换成二进制进行存储的,我们来看一下0.1和0.2转换成二进制的结果: ~~~ (0.1)10 => (00011001100110011001(1001)...)2 (0.2)10 => (00110011001100110011(0011)...)2 ~~~ 可以发现,0.1和0.2转成二进制之后都是一个无限循环的数,前面提到尾数位只能存储最多53位有效数字,这时候就必须来进行四舍五入了,而这个取舍的规则就是在IEEE 754中定义的,0.1最终能被存储的有效数字是 ~~~ 0001(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)(1001)101 + (0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)(0011)01 = 0100(1100)(1100)(1100)(1100)(1100)(1100)(1100)(1100)(1100)(1100)(1100)(1100)111 复制代码 ~~~ > 这里注意,53位的存储位指的是能存53位有效数字,因此前置的0不算,要往后再取到53位有效数字为止。 最终的这个二进制数转成十进制就是0.30000000000000004(不信的话可以找一个在线进制转换工具试一下。 # 最大安全整数 最大安全整数9007199254740991对应的二进制数如图: ![](https://box.kancloud.cn/1e9daad47daf0b9464906dbd9acc2407_690x67.png) 53位有效数字都存储满了之后,想要表示更大的数字,就只能往指数数加一位,这时候尾数因为没有多余的存储空间,因此只能补0。 ![](https://box.kancloud.cn/a9c98087f6842de5577ee9c34882d375_636x281.png) 如图所有,在指数位为53的情况下,最后一位尾数位为0的数字可以被精确表示,而最后一位尾数为为1的数字都不能被精确表示。也就是可以被精确表示和不能被精确表示的比例是`1:1`。 同理,当指数为54的时候,只有最后两位尾数为00的可以被精确表示,也就是可以被精确表示和不能被精确表示的比例是`1:3`,当有效位数达到`x(x>53)`的时候,可以被精确表示和不能被精确表示的比例将是`1 : 2^(x-53) - 1`。 可以预见的是,在指数越来越高的时候,这个指数会成指数增长,因此在Number.MAX\_SAFE\_INTEGER ~ Number.MAX\_VALUE之间可以被精确表示的整数可以说是凤毛麟角。 ### 总结 可以发现,不管是浮点数计算的计算结果错误和大整数的计算结果错误,最终都可以归结到JS的精度只有53位(尾数只能存储53位的有效数字)。那么我们在日常工作中碰到这两个问题该如何解决呢? 大而全的解决方案就是使用[mathjs](https://link.juejin.im/?target=https%3A%2F%2Fgithub.com%2Fjosdejong%2Fmathjs),看一下mathjs的输出: ~~~ math.config({ number: 'BigNumber', precision: 64 }); console.log(math.format(math.eval('0.1 + 0.2'))); // '0.3' console.log(math.format(math.eval('0.23 \* 0.34 \* 0.92'))); // '0.071944' console.log(math.format(math.eval('9007199254740991 + 2'))); // '9.007199254740993e+15' ~~~ 其实平时在遇到整型溢出的情况是非常少的,大部分场景是浮点数的计算,如果不想因为一些简单的计算引入mathjs的话,也可以自己来实现运算函数(需要考虑数字是否越界和当数字被表示成科学计数法的场景),如果懒得自己实现的话,可以使用这个1k都不到的[number-precision](https://link.juejin.im?target=https%3A%2F%2Fgithub.com%2Fnefe%2Fnumber-precision),这个工具库API简洁很多,已经可以解决浮点数的计算问题了(看了代码,对于超出Number.MAX\_SAFE\_INTEGER的数字的处理方式是抛出warning)。 # 参考资料 * [从0.1+0.2=0.30000000000000004再看JS中的Number类型](https://juejin.im/post/5a6fce10f265da3e261c3c71)