js小數運算會出現精度問題
js number類型
JS 數字類型只有number類型,number類型相當於其他強類型語言中的double類型(雙精度浮點型
),不區分浮點型和整數型。
number類型不同進制
number 有四種進制表示方法,十進制,二進制,八進制和十六進制
表示方法
- 二進制: 0B或者0b (
數字0和字母B或者小寫字母b
) ,后接1或者0表示二進制數 - 八進制: es5下禁止表示八進制數會自動轉化為十進制數,es6用
0o
,后接小於8的數字表示八進制 - 十六進制: 以
0x
或者0X
開頭,后接0-9數字和a-e五個英文字母 - 十進制:默認直接輸入0-9都是十進制數
number進制的轉換
parseInt
和 toString
- 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位小數位
符號位:用來表示數字的正負,-1^符號位數值,0為正數,1為負數
指數位:一般都用科學計數法表示數值大小,但是這里一般都是2進制的科學計數法,表示2的多少次方
小數位:科學計數法前面的數值,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 :將小數轉化為整數進行運算
- 實現思想:先將小數轉化為字符串,判斷小數部分位數,並且將運算兩邊小數同時乘以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))
- 因為浮點數轉換的時候小數乘二取整會有無限循環的情況,但是整數除二取余是不會的,所以整數部分不會出現精度丟失問題
-
第三方封裝類庫
-
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)
- math庫使用
-
bignumber,big,decimal等
- 將js原生number類型轉為bignumber,big,decimal等封裝類型,(decimal是8421 BCD編碼,bignumber是支持高精度的數據類型,實現原理?大概是用類數組存儲數據位,保持精度的可靠性)
-