JS浮點(diǎn)數(shù)精度問題及解決過程
經(jīng)典真題
- 為什么 console.log(0.2+0.1==0.3) 得到的值為 false
浮點(diǎn)數(shù)精度常見問題
在 JavaScript 中整數(shù)和浮點(diǎn)數(shù)都屬于 number 數(shù)據(jù)類型,所有數(shù)字都是以 64 位浮點(diǎn)數(shù)形式儲(chǔ)存,即便整數(shù)也是如此。 所以我們?cè)诖蛴?nbsp;1.00 這樣的浮點(diǎn)數(shù)的結(jié)果是 1 而非 1.00 。
在一些特殊的數(shù)值表示中,例如金額,這樣看上去有點(diǎn)別扭,但是至少值是正確了。
然而要命的是,當(dāng)浮點(diǎn)數(shù)做數(shù)學(xué)運(yùn)算的時(shí)候,你經(jīng)常會(huì)發(fā)現(xiàn)一些問題,舉幾個(gè)例子:
場景一:進(jìn)行浮點(diǎn)值運(yùn)算結(jié)果的判斷
// 加法 console.log(0.1 + 0.2); // 0.30000000000000004 console.log(0.7 + 0.1); // 0.7999999999999999 console.log(0.2 + 0.4); // 0.6000000000000001 console.log(2.22 + 0.1); // 2.3200000000000003 // 減法 console.log(1.5 - 1.2); // 0.30000000000000004 console.log(0.3 - 0.2); // 0.09999999999999998 // 乘法 console.log(19.9 * 100); // 1989.9999999999998 console.log(19.9 * 10 * 10); // 1990 console.log(9.7 * 100); // 969.9999999999999 console.log(39.7 * 100); // 3970.0000000000005 // 除法 console.log(0.3 / 0.1); // 2.9999999999999996 console.log(0.69 / 10); // 0.06899999999999999
場景二:將小數(shù)乘以 10 的 n 次方取整
比如將錢幣的單位,從元轉(zhuǎn)化成分,經(jīng)常寫出來的是 parseInt(yuan*100, 10)
console.log(parseInt(0.58 * 100, 10)); // 57
場景三:四舍五入保留 n 位小數(shù)
例如我們會(huì)寫出 (number).toFixed(2),但是看下面的例子:
console.log((1.335).toFixed(2)); // 1.33
在上面的例子中,我們得出的結(jié)果是 1.33,而不是預(yù)期結(jié)果 1.34。
為什么會(huì)有這樣的問題
似乎是不可思議。小學(xué)生都會(huì)算的題目,JavaScript 不會(huì)?
我們來看看其真正的原因,到底為什么會(huì)產(chǎn)生精度丟失的問題呢?
計(jì)算機(jī)底層只有 0 和 1, 所以所有的運(yùn)算最后實(shí)際上都是二進(jìn)制運(yùn)算。
十進(jìn)制整數(shù)利用輾轉(zhuǎn)相除的方法可以準(zhǔn)確地轉(zhuǎn)換為二進(jìn)制數(shù),但浮點(diǎn)數(shù)呢?
JavaScript 里的數(shù)字是采用 IEEE 754 標(biāo)準(zhǔn)的 64 位雙精度浮點(diǎn)數(shù)。
先看下面一張圖:

該規(guī)范定義了浮點(diǎn)數(shù)的格式,對(duì)于 64 位的浮點(diǎn)數(shù)在內(nèi)存中的表示,最高的 1 位是符號(hào)位,接著的 11 位是指數(shù),剩下的 52 位為有效數(shù)字,具體如下:
- 符號(hào)位 S:第 1 位是正負(fù)數(shù)符號(hào)位(sign),0 代表正數(shù),1 代表負(fù)數(shù)
- 指數(shù)位 E:中間的 11 位存儲(chǔ)指數(shù)(exponent),用來表示次方數(shù)
- 尾數(shù)位 M:最后的 52 位是尾數(shù)(mantissa),儲(chǔ)存小數(shù)部分,超出的部分自動(dòng)進(jìn)一舍零
也就是說,浮點(diǎn)數(shù)最終在運(yùn)算的時(shí)候?qū)嶋H上是一個(gè)符合該標(biāo)準(zhǔn)的二進(jìn)制數(shù)
符號(hào)位決定了一個(gè)數(shù)的正負(fù),指數(shù)部分決定了數(shù)值的大小,小數(shù)部分決定了數(shù)值的精度。
IEEE 754 規(guī)定,有效數(shù)字第一位默認(rèn)總是 1,不保存在 64 位浮點(diǎn)數(shù)之中。也就是說,有效數(shù)字總是 1.xx…xx 的形式,其中 xx…xx 的部分保存在 64 位浮點(diǎn)數(shù)之中,最長可能為 52 位。因此,JavaScript 提供的有效數(shù)字最長為 53 個(gè)二進(jìn)制位(64 位浮點(diǎn)的后 52 位 + 有效數(shù)字第一位的 1)。
既然限定位數(shù),必然有截?cái)嗟目赡堋?/p>
我們可以看一個(gè)例子:
console.log(0.1 + 0.2); // 0.30000000000000004
為了驗(yàn)證該例子,我們得先知道怎么將浮點(diǎn)數(shù)轉(zhuǎn)換為二進(jìn)制,整數(shù)我們可以用除 2 取余的方式,小數(shù)我們則可以用乘 2 取整的方式。
0.1 轉(zhuǎn)換為二進(jìn)制:
0.1 * 2,值為 0.2,小數(shù)部分 0.2,整數(shù)部分 0
0.2 * 2,值為 0.4,小數(shù)部分 0.4,整數(shù)部分 0
0.4 * 2,值為0.8,小數(shù)部分0.8,整數(shù)部分0
0.8 * 2,值為 1.6,小數(shù)部分 0.6,整數(shù)部分 1
0.6 * 2,值為 1.2,小數(shù)部分 0.2,整數(shù)部分 1
0.2 * 2,值為 0.4,小數(shù)部分 0.4,整數(shù)部分 0
從 0.2 開始循環(huán)
0.2 轉(zhuǎn)換為二進(jìn)制可以直接參考上述,肯定最后也是一個(gè)循環(huán)的情況
所以最終我們能得到兩個(gè)循環(huán)的二進(jìn)制數(shù):
0.1:0.0001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1001 1100 ...
0.2:0.0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 0011 ...
這兩個(gè)的和的二進(jìn)制就是:
sum:0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 ...
最終我們只能得到和的近似值(按照 IEEE 754 標(biāo)準(zhǔn)保留 52 位,按 0 舍 1 入來取值),然后轉(zhuǎn)換為十進(jìn)制數(shù)變成:
sum ≈ 0.30000000000000004
再例如:
console.log((1.335).toFixed(2)); // 1.33
因?yàn)?nbsp;1.335 其實(shí)是 1.33499999999999996447286321199,toFixed 雖然是四舍五入,但是是對(duì) 1.33499999999999996447286321199 進(jìn)行四五入,所以得出 1.33。
在 Javascript 中,整數(shù)精度同樣存在問題,先來看看問題:
console.log(19571992547450991); // 19571992547450990 console.log(19571992547450991===19571992547450992); // true
同樣的原因,在 JavaScript 中 number 類型統(tǒng)一按浮點(diǎn)數(shù)處理,整數(shù)是按最大 54 位來算,
- 最大( 253 - 1,Number.MAX_SAFE_INTEGER、9007199254740991)
- 最小( -(253 - 1),Number.MIN_SAFE_INTEGER、-9007199254740991)
所以只要超過這個(gè)范圍,就會(huì)存在被舍去的精度問題。
當(dāng)然這個(gè)問題并不只是在 Javascript 中才會(huì)出現(xiàn),幾乎所有的編程語言都采用了 IEEE-754 浮點(diǎn)數(shù)表示法,任何使用二進(jìn)制浮點(diǎn)數(shù)的編程語言都會(huì)有這個(gè)問題。
只不過在很多其他語言中已經(jīng)封裝好了方法來避免精度的問題,而 JavaScript 是一門弱類型的語言,從設(shè)計(jì)思想上就沒有對(duì)浮點(diǎn)數(shù)有個(gè)嚴(yán)格的數(shù)據(jù)類型,所以精度誤差的問題就顯得格外突出。
通常這種對(duì)精度要求高的計(jì)算都應(yīng)該交給后端去計(jì)算和存儲(chǔ),因?yàn)楹蠖擞谐墒斓膸靵斫鉀Q這種計(jì)算問題。
前端也有幾個(gè)不錯(cuò)的類庫:
Math.js
- Math.js 是專門為 JavaScript 和 Node.js 提供的一個(gè)廣泛的數(shù)學(xué)庫。它具有靈活的表達(dá)式解析器,支持符號(hào)計(jì)算,配有大量內(nèi)置函數(shù)和常量,并提供集成解決方案來處理不同的數(shù)據(jù)類型。
- 像數(shù)字,大數(shù)字(超出安全數(shù)的數(shù)字),復(fù)數(shù),分?jǐn)?shù),單位和矩陣。 功能強(qiáng)大,易于使用。
decimal.js
- 為 JavaScript 提供十進(jìn)制類型的任意精度數(shù)值。
big.js
- 不僅能夠支持處理 Long 類型的數(shù)據(jù),也能夠準(zhǔn)確的處理小數(shù)的運(yùn)算。
真題解答
- 為什么 console.log(0.2+0.1==0.3) 得到的值為 false
參考答案:
- 因?yàn)楦↑c(diǎn)數(shù)的計(jì)算存在 round-off 問題,也就是浮點(diǎn)數(shù)不能夠進(jìn)行精確的計(jì)算。并且:
- 不僅 JavaScript,所有遵循 IEEE 754 規(guī)范的語言都是如此;
- 在 JavaScript 中,所有的 Number 都是以 64-bit 的雙精度浮點(diǎn)數(shù)存儲(chǔ)的;
雙精度的浮點(diǎn)數(shù)在這 64 位上劃分為 3 段,而這 3 段也就確定了一個(gè)浮點(diǎn)數(shù)的值,64bit 的劃分是“1-11-52”的模式,具體來說:
- 就是 1 位最高位(最左邊那一位)表示符號(hào)位,0 表示正,1 表示負(fù);
- 11 位表示指數(shù)部分;
- 52 位表示尾數(shù)部分,也就是有效域部分
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
JavaScript構(gòu)造函數(shù)原理及實(shí)現(xiàn)流程解析
這篇文章主要介紹了JavaScript構(gòu)造函數(shù)原理及實(shí)現(xiàn)流程解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-11-11
在js代碼拼接dom對(duì)象到頁面上去的模板總結(jié)(必看)
下面小編就為大家?guī)硪黄趈s代碼拼接dom對(duì)象到頁面上去的模板總結(jié)(必看)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-02-02
JavaScript高級(jí)?ES7-ES13?新特性詳解
這篇文章主要介紹了JavaScript高級(jí)?ES7-ES13?新特性詳解,本文結(jié)合實(shí)例代碼給大家講解的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-02-02
微信小程序訂閱消息(java后端實(shí)現(xiàn))開發(fā)
這篇文章主要介紹了微信小程序訂閱消息(java后端實(shí)現(xiàn))開發(fā),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06
JavaScript數(shù)組Array的一些常用方法總結(jié)
JavaScript的Array對(duì)象是用于構(gòu)造數(shù)組的全局對(duì)象,數(shù)組是類似于列表的高階對(duì)象,下面這篇文章主要給大家介紹了關(guān)于JavaScript數(shù)組Array的一些常用方法,需要的朋友可以參考下2021-11-11
使用echart實(shí)現(xiàn)柱狀圖里的水波效果示例代碼
這篇文章主要介紹了使用echart實(shí)現(xiàn)柱狀圖里水波效果的相關(guān)資料,主要通過renderItem函數(shù)方法,返回每個(gè)數(shù)據(jù)項(xiàng)對(duì)應(yīng)的圖形元素,包括一個(gè)矩形和一個(gè)波浪路徑,需要的朋友可以參考下2026-05-05
詳解將微信小程序接口Promise化并使用async函數(shù)
這篇文章主要介紹了詳解將微信小程序接口Promise化并使用async函數(shù),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08

