0.57 * 100 === 56.99999999999999 之謎

導語 爲何 0.1 + 0.2 === 0.30000000000000004, 0.3 - 0.2 === 0.09999999999999998 ?前端

前言

在最近業務開發中, 做者偶遇到了一個與 JavaScript 浮點數相關的 Bug。git

這裏就簡單描述下背景: 在提現相關業務時, 會將展現給用戶以元爲單位的數值轉化爲以分爲單位的數值。 例如, 0.57元 轉化爲 57 分github

轉化方法很簡單正則表達式

// 小程序代碼 onInput: 監聽Input事件
onInput(e) {
    let value = e.target.value;
    //限制除數字和小數點之外的字符輸入
    if (!/^\d*\.{0,2}\d{0,2}$/.test(value)) {
        value = value
            .replace(/[^\d.]/g, '')
            .replace(/^\./g, '')
            .replace(/\.{2,}/g, '.')
            // 保留數字小數點後兩位
            .replace(/^(.*\..{2}).*$/, '$1');
    }
    //...
    this.setData({
        cash: +value * 100  // 乘100, 將元轉化爲分
    })
}

複製代碼

這段看似沒有問題的代碼, 提交給後臺時, 接口卻返回參數值格式不正確。算法

最初, 懷疑是正則表達式有疏漏, 但測試了一下沒有問題, 而後就嘗試了用戶輸入的數值 0.57, 卻發現計算值卻出人意料, 也就是題目中的 0.57 * 100 === 56.99999999999999小程序

前端開發同窗或多或少都應該看到過0.1 + 0.2 === 0.30000000000000004這個經典問題。 做者當初也抱着好奇的態度看了相關文章, 說來慚愧, 想到本身不管如何也不會開發0.1 + 0.2的業務, 也只是瞭解到了爲何會是這樣的結果就淺嘗輒止了。bash

現在踩了坑, 只能說是本身跳進了當年挖的坑, 那今天就將這個坑填上。工具

本文文章會講述如下幾個問題, 已經熟悉同窗就能夠不用看啦。測試

  1. 爲何 0.1 + 0.2 === 0.30000000000000004
  2. 爲何 0.57 * 100 === 56.99999999999999
  3. 爲何 0.57 * 1000 === 570

Why 0.1 + 0.2 === 0.30000000000000004 ?

要解答這個問題始終繞不過JavaScript中最基礎也是最核心的浮點數的格式存儲。 在JS中, 不管整數仍是小數都是Number類型, 它的實現遵循IEEE 754, 是標準的Double雙精度浮點數, 使用固定的64位來表示。ui

看到這裏你可能就不想看下去了。好好好, 那就後面再說, 這裏就用大白話簡單講解, 詳細內容在文章後面閱讀。

實際上, JS中的數字都會轉化爲二進制存儲下來, 因爲數字存儲限定了64位, 但現實世界中, 數字是無窮的, 因此必定會有數字超出這個存儲範圍。超出這個範圍的數字在存儲時就會丟失精度。

同時, 咱們都知道, 整數十進制轉二進制時, 是除以二去餘數, 這是能夠除盡的! 但咱們可能不知道的是, 小數十進制轉化爲二進制的計算方法是, 小數部分*2, 取整數部分, 直至小數部分爲0, 若是永遠不爲零, 在超過精度時的最後一位時0舍入1。

/* 0.1 轉化爲二進制的計算過程 */

0.1 * 2 = 0.2 > 取0
0.2 * 2 = 0.4 > 取0
0.4 * 2 = 0.8 > 取0
0.8 * 2 = 1.6 > 取1
0.6 * 2 = 1.2 > 取1
0.2 * 2 = 0.4 > 取0
...
後面就是循環了

複製代碼

到這裏, 咱們就能夠發現一些端倪了

// 使用toString(2), 將10進制輸出爲二進制的字符串
0.1.toString(2);
// "0.00011001100110011001100110011001100110011001100110011001100..."
0.2.toString(2);
// "0.001100110011001100110011001100110011001100110011001100110011..."

// 二進制相加結果, 因爲超過精度, 取52位, 第53位舍0進1
> "0.010011001100110011001100110011001100110011001100110011,1"
// 最後存儲下來的結果是
const s = "0.010011001100110011001100110011001100110011001100110100"
// 用算法處理一下。
a = 0;
s.split('').forEach((i, index) => { a += (+i/Math.pow(2, index+1))});
// a >> 0.30000000000000004

複製代碼

到這裏, 0.1 + 0.2 === 0.30000000000000004

以上論述過程仍有一些疑惑之處

  1. 爲何小數轉化爲二進制後, 52位之後就超過精度了?

這些都與64位雙精度浮點數是如何存儲的有關, 咱們放到最後再說。

Why 0.57 * 100 === 56.99999999999999 ?

Why 0.57 * 1000 === 570 ?

閱讀完上面一節, 對小數的乘法咱們也能夠有一些本身的猜想了。

0.57這個數值在存儲時, 自己的精度不是很準確, 咱們用toPrecision這個方法能夠獲取小數的精度。

0.57.toPrecision(55)
// "0.5699999999999999511501869164931122213602066040039062500"

複製代碼

做者最初的想法有點愚蠢, 0.57的實際值是0.56999.., 那0.57 * 100也就是0.56999... * 100, 那結果就是56.99999999999999啦。

而此時, 路總問了我一個問題, 爲何0.57 * 1000 === 570 而不是 569.99999..., 不求甚解的我只能先回答」應該是精度丟失吧」

然而, 我」小小的眼睛裏充滿了大大的疑惑」…

後來想了下, 其實咱們都知道, 計算機的乘法其實是累加計算, 並非咱們想的按位相乘。

// 僞代碼
(0.57) * 100
= (0.57) * (64 + 32 + 4)
= (0.57二進制) * (2^6 + 2^5 + 2^2)
= 0.57二進制 * 2^6 + 0.57二進制 * 2^5 + 0.57 * 2^2

複製代碼

因爲精度丟失, 這個是真的丟失啦, 在二進制轉十進制時, 結果就是56.99999…了

同理, (0.57 * 1000)也不是簡單的乘, 也是累加起來的, 只是最後精度丟失時舍0進1, 結果就是570而已。

解決問題

對於大部分業務來說, 肯定數字精度後, 使用Math.round就能夠了。 例如本文最初遇到的BUG

const value = Math.round(0.57 * 100);
複製代碼

而咱們不太肯定精度的浮點數運算, 通用的解決方案都是將小數轉化爲整數, 進行計算後, 再轉化爲小數就行了。

如下是引用[1]

/**
 * 精確加法
 */
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;
}
複製代碼

固然已經有成熟的工具庫可使用了, 例如Math.js, BigDecimal.js, number-precision等等, 使用哪一個任君挑選

IEEE754標準下的浮點數存儲

其實下面這段內容來自於Wiki

64位如圖進行劃分

64位Double浮點數存儲格式

第0位: 是符號的標誌位 第1-11位: 指數位 第12-63位: 尾數

計算公式

0.1爲例, 0.1的二進制是0.00011001100110011001100110011001100110011001100110011001100...

那麼, 首先, 該數是正數, 標誌位 sign = 0

其次, 將小數轉化爲科學計數法, 指數位-4即exponent = 2 ^10 - 4 = 1019

1.1001100110011001100110011001100110011001100110011001100... * 2^-4

因爲科學計數法, 第一個數始終是1, 因此能夠忽略存儲, 只要存後面的52位就能夠了

若是超過了52位, 就是對第53位舍0進1, 結果也就是100110011001100110011001100110011001100110011001101了。

Double精度的浮點數存儲大概就是這個樣子了, 這也解答了上述的疑惑。

以上就是本文的所有內容了。

題外: 好讀書,不求甚解;每有會意,便欣然忘食

參考

[1]JavaScript 浮點數陷阱及解法


關注【IVWEB社區】公衆號獲取每週最新文章,通往人生之巔!

相關文章
相關標籤/搜索