存储结构

javascript中number类型的存储结构统一如下:
687474703a2f2f617461322d696d672e636e2d68616e677a686f752e696d672d7075622e616c6979756e2d696e632e636f6d2f37323637613538623239383932633362373233653364366333663733393035612e706e67.png

  • sign:符号位,0为正,1为负
  • exponent:以2为底数的指数位
  • mantissa:尾数位

javascript根据以上的存储结构通过以下的公式得到十进制数值:
31601584-f65ed43e-b21f-11e7-8755-c99b48e5134c.png
其中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无限循环,用上面的存储结构进行存储则是:
捕获.PNG
当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. 1.025.toPrecision(20) // "1.0249999999999999112"

第二个坑是这个函数返回的是字符串千万别把两个数toFixed以后直接进行任何运算或者比较

浮点数精确运算

思路是先转换成整数进行运算,然后再转换成浮点数

  1. function add(num1, num2) {
  2. const len1 = (num1.toString().split('.')[1] || '').length
  3. const len2 = (num2.toString().split('.')[1] || '').length
  4. const baseNum = Math.pow(10, Math.max(len1, len2))
  5. return (num1 * baseNum + num2 * baseNum) / baseNum
  6. }

浮点数正确四舍五入

思路就是先将浮点数同上面的精确乘法,然后在用Math.round()处理,在进行精确除法,例子如下:

  1. function round(num, ratio) {
  2. const base = Math.pow(10, ratio)
  3. return divide(Math.round(multiply(num, base)), base)
  4. }

大数危机

根据上面的存储结构,JavaScript能表示的最大整数2^1024-1

其次,由于尾数的限制,最大的能精确表示的整数为 2^53 ,当要表示的数值大于这个数时,就开始不准确,因为这个数后面的数都必须通过指数来表示,这就意味着会跳掉某些数字

为了解决上面两个问题,在即将发布的ES2020中将提出BigInt内置类型。

BigInt可通过以下方式创建:

  1. let x = 111n :通过数值后加一个n来表示这个数为BigInt类型
  2. BigInt(123) :通过函数BigInt创建

参考资料:
JavaScript 浮点数陷阱及解法