如何解决JavaScript浮点数丢失的问题?

302 阅读4分钟

我们都知道计算机存储数值都是采用的是二进制,在IEEE754标准中,规定了四种表示浮点数值的方式:单精度(32位)、双精度(64位)、延伸单精确度、延伸双精确度。而Javascript中的数据基本类型只有Number为数值的,Number遵循的是IEEE754规范的,采用的是双精度存储的。

image.png

  • 符号位s:0表示正,1表示负。
  • 阶码e:浮点数的幂次,一般采用移码(64位:2^(e-1023),e的范围:[0,1023];32位:2^(e-127),e的范围:[0,127])表示,当阶码为固定值时,存储的数称为定点表示;当阶码为可变时,存储的数称为浮点表示
  • 尾数M:浮点数的小数部分,数值的有效数字。

浮点数规格化分类:对指数部分二进制进行规格化,这里以64位为例,32位也是同理的。

  • 规格化:11位指数不全为00000000000和11111111111,范围在00000000001~11111111110(1~2046)内的都是规格化数值。

    什么是规格化?整数的规格化类似于数学的科学记数法(1.0x10^n),而浮点数的规格化公式如下图,比如小数存储格式为1.Mx10^(-n)

    尾数部分是规格化表示的,最高位总是“1”,规格化时计算机会直接隐藏掉,转换的时候会再次加回去的

image.png

  • 非规格化:指数部分全为00000000000(0)。
  • 特殊值:指数部分全为11111111111,然后分两种情况分析小数部分。
    • 当52位小数部分全为1时,符号位为0表示+Infinity(正无穷),符号位为1表示-Infinity(负无穷)
    • 当52为小数不全为1时,表示NAN(Not a Number)

十进制(整数、小数)转化二进制:

  • 整数转二进制(除2取余):如12。

    image.png

  • 小数转二进制(乘2取整):如0.25。

    image.png

浮点数为什么会出现精度问题?

因为计算机中的浮点数对应数学当中的小数,64位浮点数最多可以表示2^64个数(-2^63-1~2^63-1),而在数学中[0,1]中的小数是无穷多个,计算机不能把所有小数都能计算,所以就出现了近似值,从而导致精度损失。比如像1/3、1/5、1/7、1/9、1/10这些分母说代表的小数对应的二进制都是无限循环的,但计算机不能把所有二进制都表示出来,此时就需要舍去不能表示的二进制,我们都知道整数中有四舍五入,而二进制中只有0和1,它进行的是进1舍0

image.png

  • 从32位单精度去分析0.1+0.2的值:0.1与0.2的二进制都是无限循环小数,所以无法精确显示,只能获取到一个近似值,

image.png

image.png

image.png

image.png

0.1二进制规格化浮点数:1/16 × 1.6000000238418579 = 0.1000000014901;

0.2二进制规格化浮点数:1/8 × 1.6000000238418579 = 0.200000002982;

32位单精度浮点数计算:0.1+0.2 = 0.3000000044721; 从这里我们可以看到这里的数与控制台计算的不一样,因为JavaScript使用的是双精度存储数值的,这里的小数无法用二进制描述详尽。

如何解决浮点数丢失问题?

  • Number.prototype.toFixed(x):保留小数后面x位数。

    console.log(.1 + .2);// 0.30000000000000004
    console.log((0.1 + 0.2).toFixed(1));// 0.3
    

    image.png

  • math.js:math.js是JavaScript和Node.js的一个广泛的数学库。支持数字,大数,复数,分数,单位和矩阵等数据类型的运算。

    const result = math.evaluate('0.1 + 0.2')
    // 注意:format参数precision 0-16代表精度定义了总数返回的有效位数,默认为0
    console.log(math.format(result)) // 0.30000000000000004
    console.log(math.format(result, 14)) // 0.3
    console.log(math.format(math.add(math.bignumber(0.1), math.bignumber(0.2)))) // 0.3
    
  • decimal.js:提供JavaScript的任意精度Decimal类型。

    const Decimal = require('decimal.js')
    const a = Decimal.add(0.1, 0.2)
    const b = new Decimal(0.1).add(0.2)
    console.log(a) // 0.3
    console.log(b) // 0.3
    
  • bignumber.js:一个用于任意精度算术的JavaScript库。

    const BigNumber = require('bignumber.js')
    console.log(new BigNumber(0.1).plus(0.2).toString()); // 0.3
    
  • Big.js推荐):一个用于任意精度十进制算术的小型、快速、易于使用的库。

    const Big = require('big.js')
    console.log(new Big(0.1).plus(0.2).toString()) // 0.3
    

参考资料