第二十二题:0.1 + 0.2 !== 0.3?
KieSun opened this issue · comments
原因
JavaScript 在数字相加时会将数字转成二进制再运算
0.1
转成二进制:
0.0001100110011001100110011001100110011001100110011001101....
无限循环在截断时丢失精度,导致 0.1 + 0.2
不等于 0.3
解决
利用 toFix 方法来解决精度丢失的问题
function add(a, b) {
return parseFloat((a + b).toFixed(2))
}
console.log(add(0.1, 0.2) === 0.3) //true
原因
因为 JS 采用 IEEE 754 双精度版本(64位)
浮点数用二进制表示的时候是无穷的,因为精度的问题,两个浮点数相加会造成截断丢失精度,因此再转换为十进制就出了问题
解决
export const addNum = (num1: number, num2: number) => {
let sq1;
let sq2;
let m;
try {
sq1 = num1.toString().split('.')[1].length;
} catch (e) {
sq1 = 0;
}
try {
sq2 = num2.toString().split('.')[1].length;
} catch (e) {
sq2 = 0;
}
m = Math.pow(10, Math.max(sq1, sq2));
return (Math.round(num1 * m) + Math.round(num2 * m)) / m;
};
不是原创,是作者的解决方案
原因
由于计算机中所有数据都是以二进制储存的,计算时需要把数据转换成二进制进行计算,再把结果转换成十进制。
而大多数小数的二进制都是无限循环的,根据IEEE 754标准,Number类型使用64位固定长度来表示。
其中符号位为占1位,指数位占11位,尾数位占52位。
此时0.1+0.2的二进制会丢失精度从而不等于0.3的二进制。
解决
ES6
使用ES6提供的Number.EPSILON方法
function numbersequal(a,b){
return Math.abs(a-b)<Number.EPSILON;
}
var a=0.1+0.2;
var b=0.3;
console.log(numbersequal(a,b)); //true
ES6之前
把计算数字 提升 10 的N次方倍再除以 10 的N次方。N>1.
(0.1*1000+0.2*1000)/1000===0.3 //true
原因:js使用的 IEEE 754 双精度问题
小数通过二进制表示是以无限循环存储的,浮点数标准会裁剪掉数字。
解决:
toFixed函数
const toFixed = (a,b,ratio=2)=>parseFloat((a+b).toFixed(ratio))
toFixed(0.1,0.2)
0.1 + 0.2 !== 0.3 ?
原因:
计算机数值计算采用二进制,而 JS 采用 IEEE 754 双精度版本 (64),在 0.1
转成二进制过程中,
会生成无限循环并且在截断时丢失精度,从而导致 0.1 + 0.2
不等于 0.3
。
解决:
parseFloat((0.1 + 0.2).toFixed(2))
利用 toFix 方法来解决精度丢失的问题Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON
使用 ES6 语法中的 Number.EPSILON 新属性判断
二进制运算
+ 0.00011001100110011001100110011001100110011001100110011010
- 0.0011001100110011001100110011001100110011001100110011010
二进制转十进制
0.0100110011001100110011001100110011001100110011001100111
-> 0.30000000000000004
浮点数双精度的问题 ,大数相加 ,也是同样的道理
一、分割字符串 单个相加 再拼起来
二、都扩大到整数 ,再相加 ,再除回去
原因: JS采用 IEEE 754 双精度版本 (64),并且只要采用 IEEE 754 语言的都有该问题
计算机十进制采用的是二进制表示,
0.1转化为二进制是: 0.00011001100110011001100110011001100110011001100110011010
0.2转化为二进制是:0.0011001100110011001100110011001100110011001100110011010
0.00011001100110011001100110011001100110011001100110011010 +
0.0011001100110011001100110011001100110011001100110011010 =
0.0100110011001100110011001100110011001100110011001100111
解决办法:
原生: parseFloat((0.1 + 0.2).toFixed(10))
ES6: 在es6中,Number有个新的属性EPSILON,在计算机科学技术里面,这个单词的意思为极小值
0.3 - (0.1 + 0.2 ) < Number.EPSILON // true
因为JavaScript的Number类型为双精度IEEE 754 64位浮点类型,以下答案参考MDN
Math.abs((0.1+0.2) - 0.3) < Number.EPSILON
小数相加
为什么 0.1 + 0.2 !== 0.3,请描述原因并手写解决该问题的函数
JavaScript 实现数字相加的时候会将数字转化为二进制再进行运算
所以结果是
解决方法
思路是将数字转化为字符串然后再进行相加
function addition(a, b) {
var aStr = a.toString(); // 先转化为字符串
var bStr = b.toString();
var arr1 = aStr.split('').reverse().map(Number).filter(item => !isNaN(item))
var arr2 = bStr.split('').reverse().map(Number).filter(item => !isNaN(item)) // 反转数组 准备加法运算
var maxLen = Math.max(arr1.length, arr2.length) // 然后比较两者长度
var nextNum = 0, result = [];
for (var i = 0; i < maxLen; i++) {
var value = (arr1[i] || 0) + (arr2[i] || 0) + nextNum
result.push(value % 10)
nextNum = value > 9 ? 1 : 0
}
if (nextNum === 1) {
result.push(nextNum)
}
return Number(result.reverse().join('').replace(/^0/, "0."))
}
console.log(addition(0.1, 0.2));
JS采用 IEEE 754 双精度版本 (64), 0.1 + 0.2 会被计算机转成二进制,转换过程中发生了截取,导致计算后的结果再转成 十进制时发生了精度丢失.
计算机十进制采用的是二进制表示,
0.1转化为二进制是: 0.00011001100110011001100110011001100110011001100110011010
0.2转化为二进制是:0.0011001100110011001100110011001100110011001100110011010
0.00011001100110011001100110011001100110011001100110011010 +
0.0011001100110011001100110011001100110011001100110011010 =
0.0100110011001100110011001100110011001100110011001100111
转换之后结果正好为:0.30000000000000004
解决方法:
原生:parseFloat((0.1 + 0.2).toFixed(10))
ES6: 在ES6 中,Number有个新的属性,Number.EPSILON
属性表示 1 与Number可表示的大于 1 的最小的浮点数之间的差值。
(0.1 + 0.2) - 0.3 < Number.EPSILON; // true
原因:JS 采用 IEEE 754 双精度版本(64位),计算机二进制存储值,循环的数字被裁剪了,就会出现精度丢失的问题
解决方法:
- toFixed:parseFloat((0.1 + 0.2).toFixed(1)) === 0.3 // true
- 先乘后除:(0.1 * 10 + 0.2 * 10) / 10 === 0.3 // true
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;
}
计算机中的数字都是以二进制存储的,需要将数字先转为二进制,像0.1、0.2这样的数字在转换为二进制数的时候会出现无限循环。
而js存储数据的采用的IEEE 754 双精度版本(64位),能表示并进行精确算术运算的整数范围为:[-2^53-1,2^53-1]。
对超出这个长度的数字会进行0舍1入的操作。并且浮点数运算的二进制结果也会进行舍入操作。因此得到结果会准确。
解决方法:
- 先转换为整数计算结果,在转换为小数
- (0.1 + 0.2 - 0.3) < Number.EPSILON
参考资料
原因
在JS中使用Number类型表示数字(整数和浮点数),遵循 IEEE-754 标准,通过64位二进制值来表示一个数字。JS中的数值是十进制的,但是存储到计算机底层以及进行运算的时候,都是先转换为二进制,再进行运算,再转换成十进制。如果数字转换成二进制会存在循环,裁剪后运算会导致精度缺失。
解决方式
1.将数字转成整数计算,再转换为小数
2.Math.js 、decimal.js、big.js等
3.(0.1 + 0.2 - 0.3) < Number.EPSILON
const queryDigits = function queryDigits(num) {
num += '';
let [, char = ''] = num.split('.');
return char.length;
};
const plus = function plus(num1, num2) {
num1 = +num1;
num2 = +num2;
if (isNaN(num1) || isNaN(num2)) throw new TypeError('num1/num2 must be an number!');
let num1Digits = queryDigits(num1),
num2Digits = queryDigits(num2),
baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
return (num1 * baseNum + num2 * baseNum) / baseNum;
};
console.log(plus(0.1, 0.2));
原因在于64位双精度问题
处理方式自己整理了下
const mathFuns = {
// 加法
add(num1, num2) {
let r1, r2, m;
try { r1 = num1.toString().split('.')[1].length; } catch (e) { r1 = 0; }
try { r2 = num2.toString().split('.')[1].length; } catch (e) { r2 = 0; }
m = Math.pow(10, Math.max(r1, r2));
return (Math.round(num1 * m) + Math.round(num2 * m)) / m;
},
// 减法
sub(num1, num2) {
return mathFuns.add(num1, -num2);
},
// 乘法
mul(num1, num2) {
let m = 0;
const s1 = num1.toString(), s2 = num2.toString();
try { m += s1.split('.')[1].length; } catch (e) {}
try { m += s2.split('.')[1].length; } catch (e) {}
return Number(s1.replace('.', '')) * Number(s2.replace('.', '')) / Math.pow(10, m);
},
// 除法
div(num1, num2) {
let t1 = 0, t2 = 0, r1, r2;
try { t1 += num1.toString().split('.')[1].length; } catch (e) {}
try { t2 += num2.toString().split('.')[1].length; } catch (e) {}
r1 = Number(num1.toString().replace('.', ''));
r2 = Number(num2.toString().replace('.', ''));
if (t1 > t2) r2 = r2 * Math.pow(10, t1 - t2);
if (t2 > t1) r1 = r1 * Math.pow(10, t2 - t1);
return r1 / r2;
},
};
原因:1)、JavaScript中数字的存储机制:采用的是IEEE754 双精度64位浮点数
双精度(64位)浮点数的结构:(s) * (2 ^ e) * ( m )
s: sign 符号位: 1bit
e: exponent 指数位: 11bit
m: mantissa 尾数位: 52bit
排列规则为:符号位S(1位,0为正数,1为负数) + 阶码E(8位) + 尾数M(52位)
2)、0.1和0.2都会先转成二进制再相加,相加之后再转成十进制
0.1转成二进制为
0. 0 0011 0011 0011 ....循环
0.2转成二进制为
0. 0011 0011 0011 ....循环
然后用IEEE754 双精度64位浮点数
0.1=> m = 1.1 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 010
e= -4
0.2=> m = 1.1 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 010
e= -3
0.1 + 0.2 //(先变成相同的指数再相加)
// 指数不一致时,一般是往右移,因为即使右边溢出了,损失的精度远远小于左移时的溢出
0.1=> m = 0. 11 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 010
e= -3
0.2=> m = 1.1 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 010
e= -3
0.1 + 0.2 =
m = 10. 011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 1(52位)
e= -3
m = 1. 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 1(53位)
e= -2
// 最终取52位,保留成偶数
最后转换成十进制为:0.30000000000000004
解决方法
function numTofixed(num) {
if (typeof num == 'number') {
num = parseFloat(num.toFixed(10))
}
return num;
}
numTofixed(0.1+0.2)
原因:1)、JavaScript中数字的存储机制:采用的是IEEE754 双精度64位浮点数
双精度(64位)浮点数的结构:(s) * (2 ^ e) * ( m )
s: sign 符号位: 1bit
e: exponent 指数位: 11bit
m: mantissa 尾数位: 52bit
排列规则为:符号位S(1位,0为正数,1为负数) + 阶码E(8位) + 尾数M(52位)
2)、0.1和0.2都会先转成二进制再相加,相加之后再转成十进制
0.1转成二进制为
0. 0 0011 0011 0011 ....循环
0.2转成二进制为
0. 0011 0011 0011 ....循环
然后用IEEE754 双精度64位浮点数
0.1=> m = 1.1 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 010
e= -4
0.2=> m = 1.1 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 010
e= -3
0.1 + 0.2 //(先变成相同的指数再相加)
// 指数不一致时,一般是往右移,因为即使右边溢出了,损失的精度远远小于左移时的溢出
0.1=> m = 0. 11 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 010
e= -3
0.2=> m = 1.1 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 010
e= -3
0.1 + 0.2 =
m = 10. 011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 1(52位)
e= -3
m = 1. 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 1(53位)
e= -2
// 最终取52位,保留成偶数
最后转换成十进制为:0.30000000000000004
解决方法
function numTofixed(num) {
if (typeof num == 'number') {
num = parseFloat(num.toFixed(10))
}
return num;
}
numTofixed(0.1+0.2)
JS使用IEEE 754 双精度,采用二进制表示
使用parseFloat或者Math.abs((0.1+0.2) - 0.3) < Number.EPSILON
keyword: IEEE754 、二进制、精度损失
how to solve?
(0.1+0.2-0.3)<Number.EPSILON
//or
toFixed截取多少位
parseFloat(num.toFixed(10))
原因:js在计算时会转换成二进制去运算,这换算过程出现了精度丢失
(0.1100+0.2100)/100
原因:js中小数运算会先将小数转成64位二进制数再进行运算,这样就会导致再转换的时候发生数据的截取导致精度的丢失,导致运算后的结果与预期不符。
解决:parseFloat((0.1+0.2).toFixed(2))
保留两位小数,有零的话去掉多余的0