JavaScript中number的发展历程
打开chrome控制台,输入0.2+0.4按下回车就会得到一个奇怪的结果。很多程序员知道这是浮点数计算的精度误差问题,但再具体深入到为什么就不知道了。本文就以此为契机探讨JS对数字存储和计算的原理。
要说明JS如何处理数字的原理,得先从计算机处理数字的原理说起。
计算机存储和计算整数的原理
机器数和真值
机器数就是一个整数在计算机中的二进制表示形式,由于整数有正负,所以最高位用来表示符号,正数为0负数为1。真值就是由机器数表示的真正数值了。
举个例子用8位表示一个整数比如机器数00000110,最高位为0则是正数,剩余位转化为十进制为6,则该机器数的真值就是+6。
有了机器数我们就可以在计算机中存储真值了,但是真值不能单独存在,必须能运算才有意义,那么机器数如何在计算机中运算呢?
原码,反码,补码
实际上机器数并不是只有一种表示方式,我们在上面提到的方式称为原码。可以看出原码还是比较直观的,人脑可以通过最高位判断正负后再计算出真值便可进行后续运算操作。但对于计算机来说,让计算机辨别出符号位再根据符号位不同进行四则运算会让计算机的基础电路设计变得非常复杂。因为对于四则运算来说,减去整数等于加上负数,所以为了让计算机的运算更加简单,让符号位参与运算后就可以只有加法,这样既省去了符号位判断,又可以减少运算复杂度,于是就有了反码和补码。
正数的反码等于其原码,负数的反码则是符号位不变,其他位全部取反。反码就是为了将减法转化为加法。看下面这个例子:
2 - 3 = 2 + (-3)
// 原码计算
[00000010]原 + [10000011]原 = [10000101]原 // -5 错误
// 反码计算
[00000010]反 + [11111100]反 = [11111110]反 = [10000001]原 // -1 正确
可以看到通过反码计算我们成功实现了减法转化为加法并省去了符号位判断。虽然实现了,但是还有一些问题,看下面这个例子:
3 - 3.= 3 + (-3)
// 反码计算
[00000010]反 + [11111100]反 = [11111111]反 = [10000000]原 // -0
出现了-0这个没有意义的值并且会有00000000,10000000两个不同的编码来表示0。为了解决这个问题就出现了补码。正数的补码等于原码,负数的补码则是反码对其末位加1的结果。
使用补码尝试上面两个例子:
2 - 3 = 2 + (-3)
// 补码计算
[00000010]补 + [11111101]补 = [11111111]补 = [11111110]反 = [10000001]原 // -1
3 - 3 = 3 + (-3)
// 补码计算
[00000011]补 + [11111101]补 = [00000000]补 = [00000000]原 // 0
可以看到补码完美的实现了减法到加法的转化并且省去了符号位判断。补码就是现代计算机存储和计算整数的主要方式。
计算机存储和计算小数的原理
通过上文我们了解了计算机中存储整数的策略,但是针对小数的存储上述策略就无能为力了。目前主要由两类方案存储小数。
定点数
小数点固定在机器数中间的某个特定位置,点两侧则表示真数的整数和小数部分。该方法的优点在于大部分运算实现起来和整数一样,但缺点在于表示范围过小,并且存在着严重空间浪费以及容易溢出的问题。为了解决这些过于明显的缺点,就有了浮点数。
浮点数
相比于定点数小数点的固定,浮点数的小数点则是漂浮不定的,对不同的数通过科学计数法来确定小数点的位置。先回顾一下科学计数法中的相关概念,比如表示-123.456这个数字:-1.23456 * 10^2。要描述出这样一个表示法的数字需要什么呢?
- 符号(Sign):用来表示数字的正负,上例为负,需要一位
- 尾数(Mantissa):表示有效数字的精度,必须小于底数也就是2。上例为1.23456.需要位数不确定。
- 指数(Ex):控制小数点的位置。上例为2。需要位数不确定。
可以看出浮点数表示的数字需要的三个元素中,除了符号统一需要一位来确定,另外两个元素的位数都会根据精度的不同而变化,如果不规定好位数限制就难以还原出真数,为了限制这种情况就有了IEEE 754标准。此标准规定了4种表示浮点数的方法,最常用的为单精度(32位)和双精度(64位)。
- 单精度:符号位1位,指数位8位,尾数位32位
- 双精度:符号位1位,指数位11位,尾数位52位
实际真数转化公式就可以按下式计算:
V = (-1)^S 2^E M
S,M,E这三个数值在3种对应位转化表示出来,就实现了计算机中小数的保存了。
JavaScript中的number
双精度浮点数
在JS中存储数字的方式是标准的双精度浮点数。由于科学计数法规定M的整数部分必须小于底数,在二进制中也就是2,所以M的整数部分只能是1,可以被舍去。E是一个无符号整数,长度是11位取值范围为0~2047,但科学计数法中指数又可以是负数,所以取1023为中间数,0~1022为负,1024~2047为正。最终真数转化公式就演化为如下:
V = (-1)^S 2 ^(E - 1023) (M + 1)
现在我们就可以理解文章开篇提及的浮点数计算精度问题了,0.2和0.4转化为二进制小数位会无限循环,再被双精度浮点表示时,由于尾数位限制在52位就产生了浮点误差。
BigInt
为了解决双精度浮点数精度有限的问题,目前TC39已经有了一个stage 3的提案——BigInt。
这是JS中的一个新的原始类型,可以用任意精度表示整数,即使这个数已经超出number的最大安全整数。
创建方法
将n添加到任何整数值后面即可,或者使用全局BigInt函数即可。例子如下:
// 全局函数
BigInt(Number.MAX_SAFE_INTEGER)
// n
Number.MAX_SAFE_INTEGER * 2n
运算规则
- 不允许和number进行混合运算,不过可以使用比较运算符。
- 支持常见的二元运算,按位运算,但不支持一元+运算。
-- EOF --
前端开发
」下,并被添加
「JavaScript」
标签。