存储结构
javascript中number类型的存储结构统一如下:
- sign:符号位,0为正,1为负
- exponent:以2为底数的指数位
- mantissa:尾数位
javascript根据以上的存储结构通过以下的公式得到十进制数值:
其中E有11位,所以取值范围是 [0, 2047] ,由于需要表示负数,所以 [0, 1022] 就表示为 [-1023, -1] ,1023表示为0, [1024, 2047] 则为 [1, 1024] ,表格如下
| E | 实际值 |
|---|---|
| [0,1022] | [-1023, -1] |
| 1023 | 0 |
| [1024, 2047] | [1, 1024] |
对于M,根据科学计数法,M的取值在 [1, 2) ,前面第一位就可以省略,直接取小数部分
浮点数误差
下面以十进制0.1转换为2进制为例表明浮点数误差:
首先要明确浮点数中十进制转换为二进制的方法
- 整数部分通过不断除2得到余数填充各位
- 小数部分则是通过不断乘二得到整数位
以4.5的例子为例:
整数部分:
4 / 2 = 2 余 0
2 / 2 = 1 余 0
1 / 2 = 0 余 1
所以整数部分为100
小数部分:
0.5 * 2 = 1 + 0.0
所以小数部分为1
综上,4.5的二进制数位100.1
浮点数的误差主要就处在十进制向二进制的转换中,以0.1为例,转换为二进制数为0.0001100110011001…其中1001无限循环,用上面的存储结构进行存储则是:
当javascript将以上的存储结构重新变成十进制数,则结果为:0.100000000000000005551115123126
可见此时的浮点数表示已经不准确了
但是由于javascript对十进制数的最长精度为16,所以上面即使已经不准确,实际上可表示的是 0.100000000000000 ,去掉尾部0阴差阳错刚好是0.1,但这不能表示浮点数是精确的
精度相关处理
toPrecision
这个函数用于保留有效数字,也就是从左边第一个不为0的数开始数起,返回字符串。
toFixed
这个函数用于保留小数点后n位,第一个坑是其中的四舍五入并不准确,譬如 1.025 这个数,我们期待 1.025.toFixed(2) 的结果是1.03,实际给出的结果是1.02,这是因为由于上面说到的浮点数精度问题,存储的数值实际上不是 1.025
1.025.toPrecision(20) // "1.0249999999999999112"
第二个坑是这个函数返回的是字符串,千万别把两个数toFixed以后直接进行任何运算或者比较
浮点数精确运算
思路是先转换成整数进行运算,然后再转换成浮点数
function add(num1, num2) {const len1 = (num1.toString().split('.')[1] || '').lengthconst len2 = (num2.toString().split('.')[1] || '').lengthconst baseNum = Math.pow(10, Math.max(len1, len2))return (num1 * baseNum + num2 * baseNum) / baseNum}
浮点数正确四舍五入
思路就是先将浮点数同上面的精确乘法,然后在用Math.round()处理,在进行精确除法,例子如下:
function round(num, ratio) {const base = Math.pow(10, ratio)return divide(Math.round(multiply(num, base)), base)}
大数危机
根据上面的存储结构,JavaScript能表示的最大整数为 2^1024-1 。
其次,由于尾数的限制,最大的能精确表示的整数为 2^53 ,当要表示的数值大于这个数时,就开始不准确,因为这个数后面的数都必须通过指数来表示,这就意味着会跳掉某些数字
为了解决上面两个问题,在即将发布的ES2020中将提出BigInt内置类型。
BigInt可通过以下方式创建:
let x = 111n:通过数值后加一个n来表示这个数为BigInt类型BigInt(123):通过函数BigInt创建
参考资料:
JavaScript 浮点数陷阱及解法
