搞懂js中小數運算精度問題緣由及解決辦法

js小數運算會出現精度問題


js number類型

JS 數字類型只有number類型,number類型至關於其餘強類型語言中的double類型(雙精度浮點型),不區分浮點型和整數型。javascript

number類型不一樣進制

number 有四種進製表示方法,十進制,二進制,八進制和十六進制java

表示方法es6

  • 二進制: 0B或者0b (數字0和字母B或者小寫字母b) ,後接1或者0表示二進制數
  • 八進制: es5下禁止表示八進制數會自動轉化爲十進制數,es6用0o ,後接小於8的數字表示八進制
  • 十六進制: 以0x或者0X開頭,後接0-9數字和a-e五個英文字母
  • 十進制:默認直接輸入0-9都是十進制數

number進制的轉換

parseInttoString編程

  • toString() 方法接受一個值爲 2~36 之間的整數參數指定進制,默認爲十進制,將Number轉爲String
  • parseInt() 第二個參數接受一個值爲 2~36 之間的整數參數指定進制,默認爲十進制,將String轉爲Number
// toString轉換,輸入爲Number,返回爲String
var n = 120;
n.toString(); // "120"
n.toString(2); // "1111000"
n.toString(8); // "170"
n.toString(16); // "78"
n.toString(20); // "60"
0x11.toString(); // "17"
0b111.toString(); // "7"
0x11.toString(12);// "15"
// parseInt轉換,輸入爲String,返回爲Number
parseInt('110'); // 110
parseInt('110', 2); // 6
parseInt('110', 8); // 72
parseInt('110', 16); // 272
parseInt('110', 26); // 702
// toString和parseInt結合使用能夠在兩兩進制之間轉換
// 將 a 從36進制轉爲12進制
var a = 'ra'; // 36進製表示的數
parseInt(a, 36).toString(12); // "960"

OK,扯遠了,小數,浮點數,及小數運算

因爲Js的全部數字類型都是雙精度浮點型(64位)採用 IEEE754 標準數組

64位二進制數表示一個number數字編程語言

其中 64位 = 1位符號位 + 11位指數位 + 52位小數位編碼

ass

符號位:用來表示數字的正負,-1^符號位數值,0爲正數,1爲負數es5

指數位:通常都用科學計數法表示數值大小,可是這裏通常都是2進制的科學計數法,表示2的多少次方code

小數位:科學計數法前面的數值,IEEE745標準,默認全部的該數值都轉爲1.xxxxx這種格式,優勢是能夠省略一位小數位,能夠存儲更多的數字內容,缺點是丟失精度對象

大概能夠理解爲這張圖

浮點數的運算精度丟失問題就是由於,浮點數轉化爲該標準的二進制的過程當中出現的丟失

  • 整數轉二進制

    好理解,除二取餘法,7表示爲 111 = 1x2^3 + 1x2^2 + 1x2^1

  • 問題來了,小數轉二進制!!

    因爲也須要轉化爲指數形式,例如 1/2 = 1 * 2^-1, 1/4 = 1 * 2^-2,因此小數的轉化二進制過程是經過判斷小數是否是滿 1/2,1/4,8/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
    
        0.4*2=0.8======取出整數部分0
    
        0.8*2=1.6======取出整數部分1
    
        0.6*2=1.2======取出整數部分1
    
        接下來會無限循環
    
        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.1轉化成二進制是:0.0001 1001 1001 1001…(無限循環)
    
        0.1 => 0.0001 1001 1001 1001…(無限循環)
    
        同理0.2的二進制是0.0011 0011 0011 0011…(無限循環)

OK ,轉化爲二進制以後,開始準備運算

計算機中的數字都是以二進制存儲的,二進制浮點數表示法並不能精確的表示相似0.1這樣 的簡單的數字

若是要計算 0.1 + 0.2 的結果,計算機會先把 0.1 和 0.2 分別轉化成二進制,而後相加,最後再把相加獲得的結果轉爲十進制

但有一些浮點數在轉化爲二進制時,會出現無限循環 。好比, 十進制的 0.1 轉化爲二進制,會獲得以下結果:

0.1 => 0.0001 1001 1001 1001…(無限循環)

0.2 => 0.0011 0011 0011 0011…(無限循環)

而存儲結構中的尾數部分最多隻能表示 53 位。爲了能表示 0.1,只能模仿十進制進行四捨五入了,但二進制只有 0 和 1 , 因而變爲 0 舍 1 入 。 所以,0.1 在計算機裏的二進制表示形式以下:

0.1 => 0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 101

0.2 => 0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 001

用標準計數法表示以下:

0.1 => (−1)0 × 2^4 × (1.1001100110011001100110011001100110011001100110011010)2

0.2 => (−1)0 × 2^3 × (1.1001100110011001100110011001100110011001100110011010)2

在計算浮點數相加時,須要先進行 「對位」,將較小的指數化爲較大的指數,並將小數部分相應右移:

最終,「0.1 + 0.2」 在計算機裏的計算過程以下:

通過上面的計算過程,0.1 + 0.2 獲得的結果也能夠表示爲:

(−1)0 × 2−2 × (1.0011001100110011001100110011001100110011001100110100)2=>.0.30000000000000004

經過 JS 將這個二進制結果轉化爲十進制表示:

(-1)0 * 2-2 * (0b10011001100110011001100110011001100110011001100110100 * 2**-52); //0.30000000000000004

console.log(0.1 + 0.2) ; // 0.30000000000000004

這是一個典型的精度丟失案例,從上面的計算過程能夠看出,0.1 和 0.2 在轉換爲二進制時就發生了一次精度丟失,而對於計算後的二進制又有一次精度丟失 。所以,獲得的結果是不許確的。

可是問題是:幾乎全部的編程語言浮點數都是都採用IEEE浮點數算術標準~


JAVASCRIPT中的解決辦法

  1. 原生方法類

    • 由於浮點數轉換的時候小數乘二取整會有無限循環的狀況,可是整數除二取餘是不會的,因此整數部分不會出現精度丟失問題
      • 思路1 :將小數轉化爲整數進行運算
      • 實現思想:先將小數轉化爲字符串,判斷小數部分位數,而且將運算兩邊小數同時乘以10最大小數位數,再將最後結果除以10最大小數位數
      • 代碼:代碼就不沾了,網上有許多
    • 由於小數精度太高的狀況下可能出現無限循環,出現截斷或者進位等狀況
      • 思路2:限制精度,只保留小數部分位數,減少精度出現的偏差問題
      • 方法:Number.toFixed()
      • 代碼:
        console.log((0.1 + 0.2).toFixed(12) == 0.3)
                > true
                console.log((0.1 + 0.2).toFixed(12))
                > 0.300000000000
                console.log((2.4/0.8).toFixed(12))
                > 3.000000000000
      • 注意:toFixed以後會轉換爲字符串格式,能夠再使用parseFloat轉換爲小數 parseFloat((a+b).toFixed(2))
  2. 第三方封裝類庫

    • math庫

      • math庫使用
        //統一配置math.js
            math.config({
                number: 'BigNumber',  
                // 'number' (default), 
                precision: 20         
            });
            // 轉換數字類型
            var temp = math.bignumber(a) * math.bignumber(b)
            // 提取數字類型,否則會是一個math對象
            var result = math.number(temp)
    • bignumber,big,decimal等

      • 將js原生number類型轉爲bignumber,big,decimal等封裝類型,(decimal是8421 BCD編碼,bignumber是支持高精度的數據類型,實現原理?大概是用類數組存儲數據位,保持精度的可靠性)
相關文章
相關標籤/搜索