都知道0.1+0.2 = 0.30000000000000004,那要怎麼讓它等於0.3

初見雨夜發表於2022-04-02

前言

小學數學老師教過我們,0.1 + 0.2 = 0.3,但是為什麼在我們在瀏覽器的控制檯中輸出卻是0.30000000000000004?

除了加法有這個奇怪的現象,帶小數點的減法和乘除計算也會得出意料之外的結果

console.log(0.3 - 0.1) // 0.19999999999999998
console.log(0.1 * 0.2) // 0.020000000000000004
console.log(0.3 / 0.1) // 2.9999999999999996

原因

我們都知道計算機時是通過二進位制來進行計算的,即 0 和 1

就拿 0.1 + 0.2 來說,0.1表示為0.0001100110011001...,而0.2表示為0.0011001100110011...

而在二進位制中 1 + 1 = 10,所以 0.1 + 0.2 = 0.0100110011001100...

轉成10進位制就近似表示為 0.30000000000000004

結論

簡單來說就是,浮點數轉成二進位制時丟失了精度,因此在二進位制計算完再轉回十進位制時可能會和理論結果不同

對於浮點數的四則運算,許多程式語言都會有理論值和實際值不同的問題。例如Java中也會出現類似的問題,但是Java中可以使用java.math.BigDecimal類來避免這種情況

可是JS是弱型別的語言,作者Brendan Eich自述10天內開發出JS語言,一開始設計的時候就沒有對浮點數計算有個處理的好方法

那麼在日常開發的前端專案中我們可以怎麼解決嘞?

解決方案

簡單實現

使用toFixed()<不推薦>

可以控制小數點後幾位,如果為空的話會用0補充,返回一個字串

> 0.123.toFixed(2) // '0.12'

缺點:

  • 在不同瀏覽器中得出的值可能不相同,且部分數字得不到預計的結果,並不是執行嚴格的四捨五入
// 在chrome控制檯中
> 1.014.toFixed(2) // '1.01'
> 1.215.toFixed(2) // '1.22'
> 1.105.toFixed(2) // '1.10'
> 1.115.toFixed(2) // '1.11'

乘以一個10的冪次方

把需要計算的數字乘以10的n次方,讓數值都變為整數,計算完後再除以10的n次方,這樣就不會出現浮點數精度丟失問題

> (0.1 * 10 + 0.2 *10) / 10  // 0.3

我們可以將它封裝成一個函式

mathFloat = function (float, digit) {
  const math = Math.pow(10, digit);
  return parseInt(float * math, 10) / math;
}
mathFloat(0.1 + 0.2, 3)  // 0.3

缺點:

  • JS中的儲存都是通過8位元組的double浮點型別表示的,因此它並不能準確記錄所有數字,它存在一個數值範圍

    Number.MAX_SAFE_INTEGER為 9007199254740991,而Number.MIN_SAFE_INTEGER為 -9007199254740991,超出這個範圍的話JS是無法表示的
    雖然範圍有限制,但是數值一般都夠用

較為完整的實現

加法

function mathPlus(arg1, arg2) {
  let r1, r2, m;
  try {
    r1 = arg1.toString().split(".")[1].length; // 獲取小數點後字元長度
  } catch (error) {
    r1 = 0; // 為整數狀態,r1賦0
  }
  try {
    r2 = arg2.toString().split(".")[1].length;
  } catch (error) {
    r2 = 0;
  }
  m = Math.pow(10, Math.max(r1, r2)); // 確保所有引數都為整數
  return (arg1 * m + arg2 * m) / m;
}
> mathPlus(0.1, 0.2); // 0.3
> mathPlus(1, 2); // 3

減法

function mathSubtract(arg1, arg2) {
  let r1, r2, m;
  try {
    r1 = arg1.toString().split(".")[1].length;
  } catch (error) {
    r1 = 0;
  }
  try {
    r2 = arg2.toString().split(".")[1].length;
  } catch (error) {
    r2 = 0;
  }
  m = Math.pow(10, Math.max(r1, r2));
  return ((arg1 * m - arg2 * m) / m);
}
> mathSubtract(0.3, 0.1); // 0.2
> mathSubtract(3, 1); // 2

乘法

function mathMultiply(arg1, arg2) {
  let m = 0;
  let s1 = arg1.toString();
  let s2 = arg2.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)
  );
}
> mathMultiply(0.1, 0.2); // 0.02
> mathMultiply(1, 2); // 2

除法

function mathDivide(arg1, arg2) {
  let m1 = 0;
  let m2 = 0;
  let n1 = 0;
  let n2 = 0;
  try {
    m1 = arg1.toString().split('.')[1].length;
  } catch (e) {}
  try {
    m2 = arg2.toString().split('.')[1].length;
  } catch (e) {}
  n1 = Number(arg1.toString().replace('.', ''));
  n2 = Number(arg2.toString().replace('.', ''));
   /**
   * 將除法轉換成乘法
   * n1 / n2 必為整數
   * 乘以它們的小數點後個數差
   */
  return mathMultiply(n1 / n2, Math.pow(10, m2 - m1));
}
// > 0.2 / 0.03 => 6.666666666666667
> mathDivide(0.2, 0.03); // 6.666666666666665
> mathDivide(0.3, 0.1); // 3
> mathDivide(3, 1); // 3

引入第三方庫

站在前人的肩膀上,可以前進的更快。下面這些成熟的庫封裝了很多實用的函式,雖然部分函式可能永遠不會用到

Math.js

介紹:功能強大,內建大量函式,體積較大

Github地址:https://github.com/josdejong/mathjs

star: 12.2k+

decimal.js

介紹:支援三角函式等,並支援非整數冪

Github地址:https://github.com/MikeMcl/decimal.js

star: 4.8k+

big.js

介紹:體積6k,提供了CDN

Github地址:https://github.com/MikeMcl/big.js

star: 3.9k+

number-precision

介紹:體積很小,只有1k左右

Github地址:https://github.com/nefe/number-precision

star: 3.4k+

相關文章