閱讀完本文可以了解到 0.1+0.2 為什么等于 0.30000000000000004 以及 JavaScript 中最大安全數(shù)是如何來的。
十進制小數(shù)轉(zhuǎn)為二進制小數(shù)方法
拿 173.8125 舉例如何將之轉(zhuǎn)化為二進制小數(shù)。
① 針對整數(shù)部分 173,采取 除2取余,逆序排列。
173?/?2?=?86?...?186?/?2?=?43?...?043?/?2?=?21?...?1???↑21?/?2?=?10?...?1???|?逆序排列10?/?2?=?5?...?0????|5?/?2?=?2?...?1?????|2?/?2?=?1?...?01?/?2?=?0?...?1
得整數(shù)部分的二進制為 {{10101101:0}}。
② 針對小數(shù)部分 0.8125,采用 乘2取整,順序排列。
0.8125?*?2?=?1.625??|0.625?*?2?=?1.25????|?順序排列0.25?*?2?=?0.5??????|0.5?*?2?=?1?????????↓
得小數(shù)部分的二進制為 1101。
③ 將前面兩部的結(jié)果相加,結(jié)果為 {{10101101:0}}.1101。
小心,二進制小數(shù)丟失了精度!
根據(jù)上面的知識,將十進制小數(shù) 0.1 轉(zhuǎn)為二進制:
0.1?*?2?=?0.20.2?*?2?=?0.4?//?注意這里0.4?*?2?=?0.80.8?*?2?=?1.60.6?*?2?=?1.20.2?*?2?=?0.4?//?注意這里,循環(huán)開始0.4?*?2?=?0.80.8?*?2?=?1.60.6?*?2?=?1.2...
可以發(fā)現(xiàn)有限十進制小數(shù) 0.1 卻轉(zhuǎn)化成了無限二進制小數(shù) 0.000{{11001100:0}}...,可以看到精度在轉(zhuǎn)化過程中丟失了!
順便給大家推薦一個裙,它的前面是 537,中間是631,最后就是 707。想要學習前端的小伙伴可以加入我們一起學習,互相幫助。群里每天晚上都有大神免費直播上課,如果不是想學習的小伙伴就不要加啦。
能被轉(zhuǎn)化為有限二進制小數(shù)的十進制小數(shù)的最后一位必然以 5 結(jié)尾(因為只有 0.5 * 2 才能變?yōu)檎麛?shù))。所以十進制中一位小數(shù) 0.1~0.9 當中除了 0.5 之外的值在轉(zhuǎn)化成二進制的過程中都丟失了精度。
推導 0.1 + 0.2 為何等于 0.30000000000000004
在 JavaScript 中所有數(shù)值都以 IEEE-754 標準的 64bit 雙精度浮點數(shù)進行存儲的。先來了解下 IEEE-754 標準下的雙精度浮點數(shù)。
這幅圖很關(guān)鍵,可以從圖中看到 IEEE-754 標準下雙精度浮點數(shù)由三部分組成,分別如下:
sign(符號): 占 1 bit,表示正負。
exponent(指數(shù)): 占 11 bit,表示范圍。
mantissa(尾數(shù)): 占 52 bit,表示精度,多出的末尾如果是 1 需要進位。
推薦閱讀 JavaScript 浮點數(shù)陷阱及解法,閱讀完該文后可以了解到以下公式的由來。
精度位總共是 53 bit,因為用科學計數(shù)法表示,所以首位固定的 1 就沒有占用空間。即公式中 (M + 1) 里的 1。另外公式里的 1023 是 2^11 的一半。小于 1023 的用來表示小數(shù),大于 1023 的用來表示整數(shù)。
指數(shù)可以控制到 2^1024 - 1,而精度最大只達到 2^53 - 1,兩者相比可以得出 JavaScript 實際可以精確表示的數(shù)字其實很少。
0.1 轉(zhuǎn)化為二進制為 0.000{{1100110011:0}}...,用科學計數(shù)法表示為 1.{{100110011:0}}...x2^(-4),根據(jù)上述公式, S 為 0(1 bit), E 為 -4+1023,對應(yīng)的二進制為 0{{1111111011:0}}(11 bit), M 為1001100110011001100110011001100110011001100110011010(52 bit,另外注意末尾的進位), 0.1 的存儲示意圖如下:
同理, 0.2 轉(zhuǎn)化為二進制為 0.00{{1100110011:0}}...,用科學計數(shù)法表示為 1.{{100110011:0}}...x2^(-3),根據(jù)上述公式, E 為 -3+1023,對應(yīng)的二進制為 0{{1111111100:0}}, M為 1001100110011001100110011001100110011001100110011010, 0.2 的存儲示意圖如下:
0.1+0.2 即 2^(-4) x 1.1001100110011001100110011001100110011001100110011010 與 2^(-3) x 1.1001100110011001100110011001100110011001100110011010 之和
//?計算過程0.000110011001100110011001100110011001100110011001100110100.0011001100110011001100110011001100110011001100110011010//?相加得0.01001100110011001100110011001100110011001100110011001110
0.01001100110011001100110011001100110011001100110011001110 轉(zhuǎn)化為十進制就是 0.30000000000000004。驗證完成!
JavaScript 的最大安全數(shù)是如何來的
根據(jù)雙精度浮點數(shù)的構(gòu)成,精度位數(shù)是 53bit。安全數(shù)的意思是在 -2^53~2^53 內(nèi)的整數(shù)(不包括邊界)與唯一的雙精度浮點數(shù)互相對應(yīng)。舉個例子比較好理解:
Math.pow(2,?53)?===?Math.pow(2,?53)?+?1?//?true
Math.pow(2,53) 竟然與 Math.pow(2,53)+1 相等!這是因為 Math.pow(2, 53) + 1 已經(jīng)超過了尾數(shù)的精度限制(53 bit),在這個例子中 Math.pow(2,53) 和 Math.pow(2,53)+1 對應(yīng)了同一個雙精度浮點數(shù)。所以 Math.pow(2,53) 就不是安全數(shù)了。
最大的安全數(shù)為 Math.pow(2,53)-1,即 9007199254740991。
業(yè)務(wù)中碰到的精度問題以及解決方案
了解 JavaScript 精度問題對我們業(yè)務(wù)有什么幫助呢?舉個業(yè)務(wù)場景:比如有個訂單號后端 Java 同學定義的是 long 類型,但是當這個訂單號轉(zhuǎn)換成 JavaScript 的 Number 類型時候精度會丟失了,那沒有以上知識鋪墊那就理解不了精度為什么會丟失。
順便給大家推薦一個裙,它的前面是 537,中間是631,最后就是 707。想要學習前端的小伙伴可以加入我們一起學習,互相幫助。群里每天晚上都有大神免費直播上課,如果不是想學習的小伙伴就不要加啦。
解決方案大致有以下幾種:
針對大數(shù)的整數(shù)可以考慮使用 bigint 類型(目前在 stage 3 階段)。
使用?bigNumber,它的思想是轉(zhuǎn)化成 string 進行處理,這種方式對性能有一定影響。
可以考慮使用?long.js,它的思想是將 long 類型的值轉(zhuǎn)化成兩個 32 位的雙精度類型的值。
針對小數(shù)可以考慮?JavaScript 浮點數(shù)陷阱及解法?里面提到的方案。
來源:牧云云