在浏览器控制台窗口输入如下两行代码,结果出乎意料git
0.1 + 0.2 > 0.3 // true
0.1 + 0.2 = 0.30000000000000004
0.1 * 0.1 = 0.010000000000000002
复制代码
整数采用
整数除 2 取余,直到商为 0 时为止,将余数逆序排列
,小数采用小数部分乘以 2,取整,直到获得小数部分 0 或者达到所要求的精度为止,将整数顺序排列
chrome
// 以二进制 10101101.1101 为例
// 针对整数部分 10101101 计算逻辑以下
// ← 从右往左
1 * 2^0 + 0 * 2^1 + 1 * 2^2 + 1 * 2^3 + 0 * 2^4 + 1 * 2^5 + 0 * 2^6 + 1 * 2^7
= 1 + 0 + 4 + 8 + 0 + 32 + 0 + 128
= 173
// 小数部分 1101 计算逻辑以下
// 从左往右 →
1 * 2^-1 + 1 * 2^-2 + 0 * 2^-3 + 1 * 2^-4
= 1/2 + 1/4 + 0 + 1/16
= 13/16
= 0.8125
复制代码
重点:1.01011011101 * 2^7 为二进制,将其转换为 10 进制的过程为,先将 1.01011011101 作为 2 进制转换为 10 进制,获得 1.35791015625,而后将其乘以 2^7 (也就是 1.35791015625 * 128),最后获得的十进制为 173.8125浏览器
n 位二进制可表示的最大数值范围,是 n 位的每一位都为 1,对应十进制为 2^n - 1安全
// 8 位二进制 11111111 可表示的最大值,有两种计算方法
// 一、累加
1*2^0 + 1*2^1 + 1*2^2 + 1*2^3 + 1*2^4 + 1*2^5 + 1*2^6 + 1*2^7 = 127
// 二、2^n - 1
2^8 - 1 = 127
复制代码
众所周知 JS 仅有 Number 这个数值类型,而其严格遵循 ECMAScript 规范定义的 IEEE754 64 位双精度浮点数规则。而浮点数表示方式具备如下特色:markdown
因为部分数值没法精确表示(存储),因而在运算统计后误差会愈见明显函数
IEEE 754 浮点数由三个部分组成,分别是 sign bit (符号位)、exponent bias (指数偏移值) 和 fraction (分数值),所以一个 JavaScript 的 number 表示二进制应该是以下格式:
oop
一个浮点数 (Value) 的表示其实能够这样表示: value = sign * exponent * fraction
性能
十进制转换为 IEEE 754 标准主要转换过程主要经历 3 个过程ui
十进制 0.1 转换结果以下编码
(1)将 0.1 转换为二进制 0.00011001100110011001100110011001100110011001100110011001(1001 无限循环)
(2)将上述二进制转换为科学计数法获得:2^-4 * 1.1001100110011001100110011001100110011001100110011001(1001无限循环)
(3)从科学计数法中能够获得 sign 值为 0、exponent 值为 -4,-4 + 1023(固偏移定值,64 位的状况下为 1023)= 1019 再转换为 11 位二进制为 01111111011 (4)fraction 为科学计数法小数点后面的值 1001100110011001100110011001100110011001100110011010(fraction 最长为 52 位,若小数点后的长度不够 52 位用 0 补齐,但这里超过了 52 位因此这里产生了截断,截断时因为第 53 位为 1 因此会产生进位,小数点左边的 1 计算机不会自动存储,在再次换算为 10 进制时会自动加上小数点左边的 1)
(5)最终获得 0.1 存储在计算机中的二进制为:0 01111111011 1001100110011001100110011001100110011001100110011010
在第(3)步中能够看到 0.1 在存储的过程当中被截断了,由于计算机最多只能存 52 位,因此这里就产生了精度误差,这样当再次将二进制换算为十进制时就不是原来的值
能被转化为有限二进制小数的十进制小数的最后一位必然以 5 结尾(由于只有 0.5 * 2 才能变为整数,即二进制能精确地表示位数有限且分母是 2 的倍数的小数),因此十进制中一位小数 0.1 ~ 0.9 当中除了 0.5 外的值在转化成二进制的过程当中都丢失了精度
将上图中的二进制再次转换为十进制的的步骤:
因此前面的公式具体一点能够写成下面这样:其中的 e 为指数偏移值
前面计算指数偏移值时,会加上一个固定偏移值 1023,该值的计算公式以下:
其中的 e 为存储指数的比特的长度,在 64 位双精度二进制中存储指数的比特长度 e 为 11,从而能够计算获得该固定偏移值为 1023
采用指数的实际值(例如前面的 -4)加上固定偏移值的办法表示浮点数的指数,好处是能够用长度为 e 个比特的无符号整数来表示全部的指数取值,这使得两个浮点数的指数大小的比较更为容易,实际上能够按照字典次序比较两个浮点表示的大小
指数的实际值可能为正可能为负,因此 11 比特中须要有一位用来表示符号位,若是采用补码表示的话,全体符号位 sign 和指数自身的符号位将致使不能简单的进行大小比较。因此就采用了这种加上固定偏移值的方式,中文称做阶码
因此 64 位双精度二进制中指数部分原本能表示的范围为 -1023 ~ +1023(2^10 - 1,第11位为符号位),在加上一个固定偏移值 1023 后,指数偏移值范围就为 0 ~ 2046,这样指数偏移值都为正数,在存储时就不须要关心符号的问题了,咱们就能够直接用 11 位来存储指数偏移值,最终存储时指数偏移值的范围就为 0 ~ 2^11 - 1(2047)
另外在从新转换为 10 进制时,为了还原指数实际的值,指数偏移值须要减去固定偏移值 1023,最终指数在未加上固定偏移值前的的实际值的范围为 -1023 ~ 1024,其中 -1023 用来表示 0,1024 用来表示无穷,除去这两个值指数的范围为 -1022 ~ 1023
前面的公式还须要在细分一下,一共有两个公式:
当指数偏移值不为 0 时,使用下面的公式
当指数偏移值为 0 时,使用下面的公式
sign | e 指数偏移值 = 实际值+固定偏移1023 | fraction | 计算过程 | JS 中的值 |
---|---|---|---|---|
0 | 11111111110(1023+1023) | 1111111111111111111111111111111111111111111111111111 | (-1)^0 x 2^1024=1.7976931348623157e+308 | Number.MAX_VALUE |
0 | 00000000000(-1023+1023) | 0000000000000000000000000000000000000000000000000001 | (-1)^0 x 2^-52 x 2^-1022=5e-324 | Number.MIN_VALUE 最小的正数 |
0 | 10000110011(52+1023) | 1111111111111111111111111111111111111111111111111111 | (-1)^0 x 2^53=9007199254740991 | Number.MAX_SAFE_INTEGER |
1 | 10000110011(52+1023) | 1111111111111111111111111111111111111111111111111111 | (-1)^1 x 2^53=-9007199254740991 | Number.MIN_SAFE_INTEGER |
0 | 11111111111(1024+1023) | 0000000000000000000000000000000000000000000000000000 | (-1)^0 x 1 x 2^1024 | Infinity 正无穷 |
1 | 11111111111(1024+1023) | 0000000000000000000000000000000000000000000000000000 | (-1)^1 x 1 x 2^1024 | -Infinity 负无穷 |
0 | 00000000000 | 0000000000000000000000000000000000000000000000000000 | (-1)^0 x 0 x 2^-1022 | 0 |
1 | 00000000000 | 0000000000000000000000000000000000000000000000000000 | (-1)^1 x 0 x 2^-1022 | -0 |
在计算过程当中,有一步没有写出来,以第一行为例,看看 2^1024 是怎么来的:
1.(52个1) × 2^(2046 - 1023) = 1.(52个1) × 2^1023 = (53 个 1) × 2^(1023-52) = 53 位二进制表示的十进制为 (2^53 - 1) × 2^971
还有一种方法就是直接将 1.fraction 转换为 10 进制再 ✖️ 指数部分,如上面也能够写做 1.(52个1) × 2^(2046 - 1023) = 1.9999999999999998 * 2^1023
大数危机,为何 2^53-1 是最大安全整数呢?比它大会怎样?
以 2^53 来讲明一下为何 2^53-1 是最大安全整数,安全在哪里
2^53 转二进制 => 100000000000000000000000000000000000000000000000000000(53个0)
转为科学计数法 => 1.00000000000000000000000000000000000000000000000000000(53个0)×2^53
存入计算机 => 尾数位只有52位因此截掉末尾的0只能存52个0
2^53+1 转二进制 => 100000000000000000000000000000000000000000000000000001(52个0)
转为科学计数法 => 1.00000000000000000000000000000000000000000000000000001(52个0)×2^53
存入计算机 => 尾数位只有52位因此截掉末尾的1只能存52个0
能够看出来,2^53 和 2^53+1 在计算机中的存储的分数部分、指数部分都相同,因此两个不一样的数在计算机中的存储是同样的,当大于这安全值时就可能会出现精度丢失,这样就很是的不安全了。因此 2^53-1 是 JavaScript 里面的最大安全整数
既然小数在计算机中会有精度丢失,那为何 num = 0.1 能获得 0.1 呢?
const num = 0.1 为何能获得 0.1 呢?
Number.toPrecision() 跟 toFixed 相似,表示要保留几位有效数字。前面咱们知道 0.1 在存储的过程当中实际上是丢失了精度的,由于它在转换为 2 进制时为无限循环,之因此咱们写 0.1 能获得 0.1 是由于 js 帮咱们作了处理
从图中看到咱们让 0.1 保留 25 位有效数字,得出来的结果并非 0.1,因此 js 默认帮咱们作了截断。那么这个问题就能够转化为:双精度浮点数是按什么规则来截断的呢? 在双精度浮点数的英文wiki中能够找到中能够知道
为何 例如1.335.toFixed(2) 获得的是 1.33?
1.335 在咱们存储为数字时其实存储的双精度浮点数就为 1.335,以下图中的双精度浮点数的表示,虽然在存储时会把 53 位及后面的截断,但当把下面的双精度浮点数换算为 10 进制,其实获得的就是 1.335
为何咱们调用 toPrecision 还能获得 1.335 被截断前的值呢?首先咱们存储的 1.335 是 Number 格式,Number 格式受最大存储位数的限制,因此 1.335 会被截断,可是咱们在调用 toPrecision() 时能够看到实际上是以字符串的形式表示出来的,字符串无论再长都能表示出来,因此咱们能够获得被截断前的值。当你再将截断前的值转换为 Number 时,因为受双精度浮点数存储位数的限制,存储时就又会获得截断后的值
有了以上铺垫没,既然 0.1 + 0.2 !== 0.3,所以不只是 JavaScript 会产生这种问题,只要是采用 IEEE 754 双精度浮点数编码方式来表示浮点数的都会产生这类问题。分析过程
// 0.1
e = -4;
m = 1.1001100110011001100110011001100110011001100110011010 (52位)
// 0.2
e = -3;
m = 1.1001100110011001100110011001100110011001100110011010 (52位)
复制代码
这里的 m 指的是小数点后的 52 位,而小数点前的整数部分 1 就是前面说过的隐藏位
而后把它们相加,这里有一个问题就是指数不一致时应该怎么处理,通常是往右移,由于即便右边溢出了,损失的精度远远小于左移时的溢出
e = -4;
m = 1.1001100110011001100110011001100110011001100110011010 (52位)
+
e = -3;
m = 1.1001100110011001100110011001100110011001100110011010 (52位)
复制代码
转化
e = -3;
m = 0.1100110011001100110011001100110011001100110011001101 (52位)
+
e = -3;
m = 1.1001100110011001100110011001100110011001100110011010 (52位)
复制代码
获得
e = -3;
m = 10.0110011001100110011001100110011001100110011001100111 (52位)
复制代码
保留一位整数
e = -2;
m = 1.00110011001100110011001100110011001100110011001100111 (53位)
复制代码
此时已经溢出来了(超过了 52 位),那么这时就要作四舍五入了,那怎么舍入才能与原来的数最接近呢?好比 1.101 要保留 2 位小数则结果有多是 1.10 和 1.11,这时两个都是同样近,取哪个呢?规则是保留偶数的那一个,在这里就是保留 1.10
回到上面以前,结果就是
m = 1.0011001100110011001100110011001100110011001100110100 (52位)
复制代码
而后获得最终的二进制数
2 ^ -2 * 1.0011001100110011001100110011001100110011001100110100
= 0.010011001100110011001100110011001100110011001100110100
复制代码
最终转化为十进制就是:0.30000000000000004
根据规格,Number.EPSILON 表示 1 与大于 1 的最小浮点数之间的差,Number.EPSILON 其实是 JavaScript 可以表示的最小精度,偏差若是小于这个值就能够认为已经没有意义了,即不存在偏差
比较两个数字与 Number.EPSILON 之间的绝对差值
function numbersEqual(num1, num2) {
return Math.abs(num1 - num2) < Number.EPSILON
}
const a = 0.1+0.2, b=0.3;
console.log(numbersEqual(a, b)); // true
复制代码
考虑兼容性问题,在 chrome 中支持这个属性,但 IE 并不支持(IE10 不兼容),因此还要解决 IE 的不兼容问题
Number.EPSILON=(function(){ //解决兼容性问题
return Number.EPSILON?Number.EPSILON:Math.pow(2,-52);
})();
//上面是一个自调用函数,当 JS 文件刚加载到内存中就会去判断并返回一个结果,相比
//if(!Number.EPSILON){
// Number.EPSILON=Math.pow(2,-52);
//}
// 这种代码更节约性能也更美观
function numbersequal(a,b){
return Math.abs(a-b) < Number.EPSILON;
}
// 接下来再判断
const a=0.1+0.2, b=0.3;
console.log(numbersequal(a, b)); // true
复制代码
parseFloat((数学表达式).toFixed(digits)); // toFixed() 精度参数须在 0 与20 之间
// 运行
parseFloat((0.1 + 0.2).toFixed(10))// 结果为 0.3
parseFloat((0.3 / 0.1).toFixed(10)) // 结果为 3
parseFloat((0.7 * 180).toFixed(10))// 结果为 126
parseFloat((1.0 - 0.9).toFixed(10)) // 结果为 0.1
parseFloat((9.7 * 100).toFixed(10)) // 结果为 970
parseFloat((2.22 + 0.1).toFixed(10)) // 结果为 2.32
复制代码
function add(num1, num2) {
const num1Digits = (num1.toString().split('.')[1] || '').length;
const num2Digits = (num2.toString().split('.')[1] || '').length;
const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
return (num1 * baseNum + num2 * baseNum) / baseNum;
}
function roundFractional(x, n) {
return Math.round(x * Math.pow(10, n)) / Math.pow(10, n);
}
复制代码
math.js、D.js、bigNumber.js、decimal.js、big.js 等