双精度浮点数

JS 用双精度浮点数 double 储存 Number
浮点数是一种对于实数的近似值的表示法

double 储存规则
正负 + 指数 + 精度

十进制 78.375
二进制1001 110.011

  • 小数部分由十进制转为二进制,不一定可以转的很规整,例如十进制0.1被专为二进制0.0001100110011001100(1100无限循环)。因此需要舍去或进位,从而形成误差
  • 1001 110.011 科学记数法为 1.001 110 011 * 2^6。其中前者一定以1开头,所以可以只记录小数部分;同理后者底数一定是2,只记录指数部分即可。再加上正负数,就是 double 的三个部分了

精度部分

精度部分有52位,因此把指数放大,最大可以精确储存 2^53整数值(记得前一段被忽略的开头的1也算一位)
Math.pow(2,53) === 9007199254740992 是 JS 可以储存的最大整数。再往大计算,就不会精确

指数部分

指数部分有11位,最大值为2^11-1 = 2047。由于指数有正有负,但有符号计算比无符号计算麻烦,因此存为无符号整数,其中分出一半表示负数部分,因此指数部分的值范围是 2^-1023 到 2^1024

::TODO:: 一个猜测:double 能表示的最大值 为 1 * 1.111111111(52位)* 2^1024
但是 JS 里 Math.pow(2,1024) 就返回了 Infinity,可以理解为 Number 能表示的最大值其实小于 double 真正能表示的最大值?

数值的表示法

字面量和科学计数法两种

字面量

JS 有四种进制来表示字面量:二进制八进制十进制十六进制

  • 十进制:没有前导0的数值
  • 二进制:有前缀 0b0B的数值 b = Binary
  • 八进制:有前缀 0o0O的数值 o = Octal
    • 前导0也可以表示八进制
    • 010 表示十进制的 8
    • 这个表示法容易造成混乱,已被 ES5 的严格模式、ES6 废弃。但为了兼容,浏览器依然支持这种表示
  • 十六进制:有前缀0x0X的数值 x = Hex?

科学记数法

以下两种场景下会表示为科学计数法

  • 小数点前有超过(大于等于)21位数字
    1234567890123456789012 => 1.2345678901234568e+21
    ::TODO::此时整数超过了 Number 可精确表示的最大整数,可以看到e前边的小数精度不够,没有完整储存原本的数字
  • 小数点后的0大于等于6个
    0.0000001 => 1e-7

通过小写e或者大写E后边加一个整数,用来表示指数部分。

注意被转为科学计数法后,toString 方法也会输出带e的字符串
1234567890123456789012.toString() => '1.2345678901234568e+21'

一句话概括0.1+0.2!=0.3

JS 中 Number 全部用双精度浮点数 double 储存,在十进制小数部分转为二进制小数时,可能出现除不尽的情况,小数部分无限长(应该是无限循环了),限于double储存结构,只能储存有限位数,因此存下来的会是有误差的值。运算时也是二机制数字之间运算,因此结果是不精确的。

解决方案

分场景讨论这个问题

运算类

一个经典场景,元转分问题
0.58 * 100 === 57.99999999999999
一般场景下分就是精度极限了,不会再往后有小数,因此可以 Math.round 四舍五入,拿到整数分
Math.round(0.58 * 100) === 58

上边是一种把小数转为整数的方案。更通用一些,涉及运算都需要转为整数。

可能遇到科学计数法和最大数问题

转为字符串运算

最大值问题

参考文档

数值 - JavaScript 教程 - 网道
前端新能源 - 知乎