不知道你们在使用JS的过程当中有没有发现某些浮点数运算的时候,获得的结果存在精度问题:好比0.1 + 0.2 = 0.30000000000000004以及7 * 0.8 = 5.6000000000000005等等。
bash
到底是什么缘由形成了这个问题?其实是由于计算机内部的信息都是由二进制方式表示的,即0和1组成的各类编码,但因为某些浮点数没办法用二进制准确的表示出来,也就带来了一系列精度问题。固然这也不是JS独有的问题。
框架
接下来让咱们以 0.1+0.2 为例,深刻理解一下浮点数的运算方法,以及使用JS时应该如何规避这个问题。这个问题很基础,但也颇有了解的必要,你们就当是复习一下《计算机组成原理》吧。
测试
经过后面的几个小章节,将会大体为你们介绍如下几个方面内容:ui
● 浮点数的二进制表示方式
● IEEE 754 标准是什么
● 避开浮点数计算精度问题的方案
● 测试框架(Mocha)的基本用法编码
① 整数部分:除2取余数,若商不为0则继续对它除2,当商为0时则将全部余数逆序排列; spa
② 小数部分:乘2取整数部分,若小数不为0则继续乘2,直至小数部分为0将取出的整数位正序排列。(若小数部分没法为零,根据有效位数要求取得相应数值,位数后一位0舍1入进行取舍) 设计
利用上述方法,咱们尝试一下将0.1转成二进制:3d
0.1 * 2 = 0.2 - - - - - - - - - - 取0 code
0.2 * 2 = 0.4 - - - - - - - - - - 取0 cdn
0.4 * 2 = 0.8 - - - - - - - - - - 取0
0.8 * 2 = 1.6 - - - - - - - - - - 取1
0.6 * 2 = 1.2 - - - - - - - - - - 取1
0.2 * 2 = 0.4 - - - - - - - - - - 取0
......
算到这就会发现小数部分再怎么继续乘都不会等于0,因此二进制是没办法精确表示0.1的。 那么0.1的二进制表示是:0.000110011......0011...... (0011无限循环) 而0.2的二进制表示则是:0.00110011......0011...... (0011无限循环) 而具体应该保存多少位数,则须要根据使用的是什么标准来肯定,也就是下一节所要讲到的内容。
IEEE 754 标准是IEEE二进位浮点数算术标准(IEEE Standard for Floating-Point Arithmetic)的标准编号。IEEE 754 标准规定了计算机程序设计环境中的二进制和十进制的浮点数自述的交换、算术格式以及方法。 根据IEEE 754标准,任意一个二进制浮点数均可以表示成如下形式:
S为数符,它表示浮点数的正负(0正1负);M为有效位(尾数);E为阶码,用移码表示,阶码的真值都被加上一个常数(偏移量)。 尾数部分M一般都是规格化表示的,即非"0"的尾数其第一位老是"1",而这一位也称隐藏位,由于存储时候这一位是会被省略的。好比保存1.0011时,只保存0011,等读取的时候才把第一位的1加上去,这样作至关于多保存了1位有效数字。
经常使用的浮点格式有:
① 单精度:
其真值为:
② 双精度:
其真值为:
JavaScript只有一种数字类型number,而number使用的就是IEEE 754双精度浮点格式。
依据上述规则,接下来咱们就来看看 JS 是如何存储 0.1 和 0.2 的:
0.1 是正数,因此符号位是0;
而其二进制位是 0.000110011......0011...... ( 0011 无限循环),进行规格化后为1.10011001......1001(1)*2^-4,根据0舍1入的规则,最后的值为
2^-4 * 1.1001100110011001100110011001100110011001100110011010
而指数 E = -4 + 1023 = 1019 由此可得,JS中 0.1 的二进制存储格式为(符号位用逗号分隔,指数位用分号分隔): 0,01111111011;1001100110011001100110011001100110011001100110011010
0.2 则为0,01111111100;1001100110011001100110011001100110011001100110011010
0.1 => 0,01111111011;1001100110011001100110011001100110011001100110011010 0.2 => 0,01111111100;1001100110011001100110011001100110011001100110011010 浮点数的加减运算按如下几步进行: ① 对阶,使两数的小数点位置对齐(也就是使两数的阶码相等)。 因此要先求阶差,阶小的尾数要根据阶差来右移(尾数位移时可能会发生数丢失的状况,影响精度) 由于0.1和0.2的阶码和尾数均为正数,因此它们的原码、反码及补码都是同样的。(使用补码进行运算,计算过程当中使用双符号) △阶差(补码) = 00,01111111011 - 00,01111111100 = 00,01111111011 + 11,10000000100 = 11,11111111111 由上可知△阶差为-1,也就是0.1的阶码比0.2的小,因此要把0.1的尾数右移1位,阶码加1(使0.1的阶码和0.2的一致) 最后0.1 => 0,01111111100;1100110011001100110011001100110011001100110011001101(0) 注:要注意0舍1入的原则。之因此右移一位,尾数补的是1,是由于隐藏位的数值为1(默认是不存储的,只有读取的时候才加上
② 尾数求和
0.1100110011001100110011001100110011001100110011001101 + 1.1001100110011001100110011001100110011001100110011010 —————————————————————————————— 10.0110011001100110011001100110011001100110011001100111
③ 规格化 针对步骤②的结果,须要右规(即尾数右移1位,阶码加1) sum = 0.1 + 0.2 = 0,01111111101;1.0011001100110011001100110011001100110011001100110011(1) 注:右规操做,可能会致使低位丢失,引发偏差,形成精度问题。因此就须要步骤④的舍入操做
④ 舍入(0舍1入)
sum = 0,01111111101;1.0011001100110011001100110011001100110011001100110100
⑤ 溢出判断
根据阶码判断浮点运算是否溢出。而咱们的阶码 01111111101 即不上溢,也不下溢。
至此,0.1+0.2的运算就已经结束了。接下来,咱们一块儿来看看上面计算获得的结果,它的十进制数是多少。
<1> 先将它非规格化,获得二进制形式:
sum = 0.010011001100110011001100110011001100110011001100110100
<2> 再将其转成十进制
sum = 2^2 + 2^5 + 2^6 + ... + 2^52 = 0.30000000000000004440892098500626
如今你应该明白JS中 0.30000000000000004 这个结果怎么来的吧。
Q2:计算机运算为什么要使用补码?
A2:能够简化计算机的运算步骤,且只用设加法器,如作减法时若能找到与负数等价的正数来代替该负数,就能够把减法操做用加法代替。而采用补码,就能达到这个效果。
个人思路就是将小数转成整数来运算,以后再转回小数。代码也比较简单,就直接贴出来了。
'use strict'
var accAdd = function(num1, num2) {
num1 = Number(num1);
num2 = Number(num2);
var dec1, dec2, times;
try { dec1 = countDecimals(num1)+1; } catch (e) { dec1 = 0; }
try { dec2 = countDecimals(num2)+1; } catch (e) { dec2 = 0; }
times = Math.pow(10, Math.max(dec1, dec2));
// var result = (num1 * times + num2 * times) / times;
var result = (accMul(num1, times) + accMul(num2, times)) / times;
return getCorrectResult("add", num1, num2, result);
// return result;
};
var accSub = function(num1, num2) {
num1 = Number(num1);
num2 = Number(num2);
var dec1, dec2, times;
try { dec1 = countDecimals(num1)+1; } catch (e) { dec1 = 0; }
try { dec2 = countDecimals(num2)+1; } catch (e) { dec2 = 0; }
times = Math.pow(10, Math.max(dec1, dec2));
// var result = Number(((num1 * times - num2 * times) / times);
var result = Number((accMul(num1, times) - accMul(num2, times)) / times);
return getCorrectResult("sub", num1, num2, result);
// return result;
};
var accDiv = function(num1, num2) {
num1 = Number(num1);
num2 = Number(num2);
var t1 = 0, t2 = 0, dec1, dec2;
try { t1 = countDecimals(num1); } catch (e) { }
try { t2 = countDecimals(num2); } catch (e) { }
dec1 = convertToInt(num1);
dec2 = convertToInt(num2);
var result = accMul((dec1 / dec2), Math.pow(10, t2 - t1));
return getCorrectResult("div", num1, num2, result);
// return result;
};
var accMul = function(num1, num2) {
num1 = Number(num1);
num2 = Number(num2);
var times = 0, s1 = num1.toString(), s2 = num2.toString();
try { times += countDecimals(s1); } catch (e) { }
try { times += countDecimals(s2); } catch (e) { }
var result = convertToInt(s1) * convertToInt(s2) / Math.pow(10, times);
return getCorrectResult("mul", num1, num2, result);
// return result;
};
var countDecimals = function(num) {
var len = 0;
try {
num = Number(num);
var str = num.toString().toUpperCase();
if (str.split('E').length === 2) { // scientific notation
var isDecimal = false;
if (str.split('.').length === 2) {
str = str.split('.')[1];
if (parseInt(str.split('E')[0]) !== 0) {
isDecimal = true;
}
}
let x = str.split('E');
if (isDecimal) {
len = x[0].length;
}
len -= parseInt(x[1]);
} else if (str.split('.').length === 2) { // decimal
if (parseInt(str.split('.')[1]) !== 0) {
len = str.split('.')[1].length;
}
}
} catch(e) {
throw e;
} finally {
if (isNaN(len) || len < 0) {
len = 0;
}
return len;
}
};
var convertToInt = function(num) {
num = Number(num);
var newNum = num;
var times = countDecimals(num);
var temp_num = num.toString().toUpperCase();
if (temp_num.split('E').length === 2) {
newNum = Math.round(num * Math.pow(10, times));
} else {
newNum = Number(temp_num.replace(".", ""));
}
return newNum;
};
var getCorrectResult = function(type, num1, num2, result) {
var temp_result = 0;
switch (type) {
case "add":
temp_result = num1 + num2;
break;
case "sub":
temp_result = num1 - num2;
break;
case "div":
temp_result = num1 / num2;
break;
case "mul":
temp_result = num1 * num2;
break;
}
if (Math.abs(result - temp_result) > 1) {
return temp_result;
}
return result;
};复制代码
基本用法:
加法: accAdd(0.1, 0.2) // 获得结果:0.3
减法: accSub(1, 0.9) // 获得结果:0.1
除法: accDiv(2.2, 100) // 获得结果:0.022
乘法: accMul(7, 0.8) // 获得结果:5.6
countDecimals()方法:计算小数位的长度
convertToInt()方法:将小数转成整数
getCorrectResult()方法:确认咱们的计算结果无误,以防万一
JS浮点数计算精度问题是由于某些小数无法用二进制精确表示出来。JS使用的是IEEE 754双精度浮点规则。 而规避浮点数计算精度问题,可经过如下几种方法:
● 调用round() 方法四舍五入或者toFixed() 方法保留指定的位数(对精度要求不高,可用这种方法)
● 将小数转为整数再作计算,即前文提到的那个简单的解决方案
● 使用特殊的进制数据类型,如前文提到的bignumber(对精度要求很高,可借助这些相关的类库)