JavaScript 精度丟失問題

// 1. 兩數相加
// 0.1 + 0.2 = 0.30000000000000004
// 0.7 + 0.1 = 0.7999999999999999
// 0.2 + 0.4 = 0.6000000000000001
// 2.22 + 0.1 = 2.3200000000000003

// 2. 兩數相減
// 1.5 - 1.2 = 0.30000000000000004
// 0.3 - 0.2 = 0.09999999999999998

// 3. 兩數相乘
// 19.9 * 100 = 1989.9999999999998
// 19.9 * 10 * 10 = 1990
// 1306377.64 * 100 = 130637763.99999999
// 1306377.64 * 10 * 10 = 130637763.99999999
// 0.7 * 180 = 125.99999999999999

// 4. 不同的數卻相等
// 1000000000000000128 === 1000000000000000129

計算機的底層實現就沒法徹底精確表示一個無限循環的數,並且可以存儲的位數也是有限制的,因此在計算過程當中只能捨去多餘的部分,獲得一個儘量接近真實值的數字表示,因而形成了這種計算偏差。html

好比在 JavaScript 中計算0.1 + 0.2時,十進制的0.1和0.2都會被轉換成二進制,但二進制並不能徹底精確表示轉換結果,由於結果是無限循環的。前端

// 百度進制轉換工具
0.1 -> 0.0001100110011001...
0.2 -> 0.0011001100110011...

In JavaScript, Number is a numeric data type in the double-precision 64-bit floating point format (IEEE 754). In other programming languages different numeric types can exist, for examples: Integers, Floats, Doubles, or Bignums.git

根據 MDN這段關於Number的描述 能夠得知,JavaScript 裏的數字是採用 IEEE 754 標準的 64 位雙精度浮點數。該規範定義了浮點數的格式,最大最小範圍,以及超過範圍的舍入方式等規範。因此只要不超過這個範圍,就不會存在捨去,也就不會存在精度問題了。好比:github

// Number.MAX_SAFE_INTEGER 是 JavaScript 裏能表示的最大的數了,超出了這個範圍就不能保證計算的準確性了
var num = Number.MAX_SAFE_INTEGER;
num + 1 === num +2 // = true

實際工做中咱們也用不到這麼大的數或者是很小的數,也應該儘可能把這種對精度要求高的計算交給後端去計算,由於後端有成熟的庫來解決這個計算問題。前端雖然也有相似的庫,可是前端引入一個這樣的庫代價太大了。web

排除直接使用的數太大或過小超出範圍,出現這種問題的狀況基本是浮點數的小數部分在轉成二進制時丟失了精度,因此咱們能夠將小數部分也轉換成整數後再計算。網上不少帖子貼出的解決方案就是這種:後端

var num1 = 0.1
var num2 = 0.2
(num1 * 10 + num2 * 10) / 10 // = 0.3

可是這樣轉換整數的方式也是一種浮點數計算,在轉換的過程當中就可能存在精度問題,好比:數組

1306377.64 * 10 // = 13063776.399999999
1306377.64 * 100 // = 130637763.99999999
var num1 = 2.22;
var num2 = 0.1;
(num1 * 10 + num2 * 10) / 10 // = 2.3200000000000003

因此不要直接經過計算將小數轉換成整數,咱們能夠經過字符串操做,移動小數點的位置來轉換成整數,最後再一樣經過字符串操做轉換回小數:wordpress

/**
 * 經過字符串操做將一個數放大或縮小指定倍數
 * @num 被轉換的數
 * @m   放大或縮小的倍數,爲正表示小數點向右移動,表示放大;爲負反之
 */
function numScale(num, m) {
  // 拆分整數、小數部分
  var parts = num.toString().split('.');
  // 原始值的整數位數
  const integerLen = parts[0].length;
  // 原始值的小數位數
  const decimalLen = parts[1] ? parts[1].length : 0;
  
  // 放大,當放大的倍數比原來的小數位大時,須要在數字後面補零
  if (m > 0) {
    // 補多少個零:m - 原始值的小數位數
    let zeros = m - decimalLen;
    while (zeros > 0) {
      zeros -= 1;
      parts.push(0);
    }
  // 縮小,當縮小的倍數比原來的整數位大時,須要在數字前面補零
  } else {
    // 補多少個零:m - 原始值的整數位數
    let zeros = Math.abs(m) - integerLen;
    while (zeros > 0) {
      zeros -= 1;
      parts.unshift(0);
    }
  }

  // 小數點位置,也是整數的位數: 
  //    放大:原始值的整數位數 + 放大的倍數
  //    縮小:原始值的整數位數 - 縮小的倍數
  var index = integerLen + m;
  // 將每一位都拆到數組裏,方便插入小數點
  parts = parts.join('').split('');
  // 當爲縮小時,由於可能會補零,因此使用原始值的整數位數
  // 計算出的小數點位置可能爲負,這個負數應該正好是補零的
  // 個數,因此小數點位置應該爲 0
  parts.splice(index > 0 ? index : 0, 0, '.');

  return parseFloat(parts.join(''));
}
/**
 * 獲取小數位數
 */
function getExponent(num) {
  return Math.floor(num) === num ?
    0 : num.toString().split('.')[1].length;
}

/**
 * 兩數相加
 */
function accAdd(num1, num2) {
  const multiple = Math.max(getExponent(num1), getExponent(num2));
  return numScale(numScale(num1, multiple) + numScale(num2, multiple), multiple * -1);
}

測試用例:工具

describe('accAdd', function() {
  it('(0.1, 0.2) = 0.3', function() {
    assert.strictEqual(0.3, _.accAdd(0.1, 0.2))
  })
  it('(2.22, 0.1) = 2.32', function() {
    assert.strictEqual(2.32, _.accAdd(2.22, 0.1))
  })
  it('(11, 11) = 22', function() {
    assert.strictEqual(22, _.accAdd(11, 11))
  })
})
相關文章
相關標籤/搜索