KieSun / all-of-frontend

你想知道的前端内容都在这

Home Page:https://yuchengkai.cn/docs/frontend

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

第二十二题:0.1 + 0.2 !== 0.3?

KieSun opened this issue · comments

commented

为什么 0.1 + 0.2 !== 0.3,请描述原因并手写解决该问题的函数。

去答题

新建了一个大厂真题每日打卡群,有意愿学习打卡的再进,请备注打卡

原因

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
commented

原因

因为 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
commented

原因: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

image

解决:

  1. parseFloat((0.1 + 0.2).toFixed(2)) 利用 toFix 方法来解决精度丢失的问题
  2. 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

image
转换之后结果正好为:0.30000000000000004

解决办法:
原生: 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 实现数字相加的时候会将数字转化为二进制再进行运算
所以结果是
image

解决方法

思路是将数字转化为字符串然后再进行相加

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;
          }
commented

计算机中的数字都是以二进制存储的,需要将数字先转为二进制,像0.1、0.2这样的数字在转换为二进制数的时候会出现无限循环。
而js存储数据的采用的IEEE 754 双精度版本(64位),能表示并进行精确算术运算的整数范围为:[-2^53-1,2^53-1]。
对超出这个长度的数字会进行0舍1入的操作。并且浮点数运算的二进制结果也会进行舍入操作。因此得到结果会准确。
解决方法:

  1. 先转换为整数计算结果,在转换为小数
  2. (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