本文作者為 360 奇舞團(tuán)前端開發(fā)工程師
在使用 JavaScript 處理運(yùn)算時(shí),有時(shí)會(huì)碰到數(shù)字運(yùn)算結(jié)果不符合預(yù)期的情況,比如經(jīng)典的 0.1 + 0.2 不等于 0.3。當(dāng)然這種問題不只存在于 JavaScript,不過編程語言的一些原理大致相通,我以 JavaScript 為例解釋這種問題,并說明前端如何盡可能保證數(shù)字精確。
let a = 0.1,b=0.2,c=0.3
console.log(a + b === c) //false
1. 計(jì)算機(jī)數(shù)字如何存儲(chǔ)
理解類似問題的基礎(chǔ),首先要理解計(jì)算機(jī)數(shù)字的處理方式。計(jì)算機(jī)的一切信息都是二進(jìn)制,數(shù)字也不例外,所有數(shù)字都是一段二進(jìn)制。
在 JavaScript 中存儲(chǔ)數(shù)字的二進(jìn)制有 64 位,即我們常說的 64 位雙精度浮點(diǎn)型數(shù)字。每個(gè)數(shù)字對(duì)應(yīng)的 64 位二進(jìn)制分為三段:符號(hào)位、指數(shù)位、尾數(shù)位。
其中符號(hào)位在六十四位的第一位,0 表示正數(shù),1 表示負(fù)數(shù)。符號(hào)位之后的 11 位是指數(shù)位,決定了數(shù)字的范圍。指數(shù)位之后的 52 位是尾數(shù)位,決定了數(shù)字的精度。
在 JavaScript 中,雙精度浮點(diǎn)型的數(shù)轉(zhuǎn)化成二進(jìn)制的數(shù)保存,讀取時(shí)根據(jù)指數(shù)位和尾數(shù)位的值轉(zhuǎn)化成雙精度浮點(diǎn)數(shù)。
比如說存儲(chǔ) 8.8125 這個(gè)數(shù),它的整數(shù)部分的二進(jìn)制是 1000,小數(shù)部分的二進(jìn)制是 1101。這兩部分連起來是 1000.1101,但是存儲(chǔ)到內(nèi)存中小數(shù)點(diǎn)會(huì)消失,因?yàn)橛?jì)算機(jī)只能存儲(chǔ) 0 和 1。
1000.1101 這個(gè)二進(jìn)制數(shù)用科學(xué)計(jì)數(shù)法表示是 1.0001101 * 2^3,這里的 3 (二進(jìn)制是 0011)即為指數(shù)。
可使用如下代碼查看:
function getBinaryRepresentation(number) {
const buffer = new ArrayBuffer(8); // 創(chuàng)建一個(gè)包含8字節(jié)的 ArrayBuffer
const view = new DataView(buffer); // 創(chuàng)建一個(gè)DataView以便訪問內(nèi)存中的數(shù)據(jù)
view.setFloat64(0, number); // 將浮點(diǎn)數(shù)寫入到內(nèi)存中
// 讀取內(nèi)存中的字節(jié),并將其轉(zhuǎn)換為二進(jìn)制字符串
const binaryString = Array.from(new Uint8Array(buffer))
.map(byte => byte.toString(2).padStart(8, '0'))
.join('');
// 將二進(jìn)制字符串分割為符號(hào)位、指數(shù)位和尾數(shù)位
const signBit = binaryString[0];
const exponentBits = binaryString.substring(1, 12);
const mantissaBits = binaryString.substring(12,64);
return { signBit, exponentBits, mantissaBits };
}
const number = 8.8125; // 要展示的數(shù)字
const { signBit, exponentBits, mantissaBits } = getBinaryRepresentation(number);
console.log(`符號(hào)位: ${signBit}`);
console.log(`指數(shù)位: ${exponentBits}`);
console.log(`尾數(shù)位: ${mantissaBits}`);
符號(hào)位: 0
指數(shù)位: 10000000010
尾數(shù)位: 0001101000000000000000000000000000000000000000000000
現(xiàn)在我們很容易判斷符號(hào)位是 0,尾數(shù)位就是科學(xué)計(jì)數(shù)法的小數(shù)部分 0001101。指數(shù)位用來存儲(chǔ)科學(xué)計(jì)數(shù)法的指數(shù),此處為 3。指數(shù)位有正負(fù),11 位指數(shù)位表示的指數(shù)范圍是 -1023~1024,所以指數(shù) 3 的指數(shù)位存儲(chǔ)為 1026(3 + 1023)。
可以判斷 JavaScript 數(shù)值的最大值為 53 位二進(jìn)制的最大值:2^53 -1。
PS:科學(xué)計(jì)數(shù)法中小數(shù)點(diǎn)前的 1 可以省略,因?yàn)檫@一位永遠(yuǎn)是 1。比如 0.5 二進(jìn)制科學(xué)計(jì)數(shù)為 1.00 * 2^-1。
2. 為什么會(huì)產(chǎn)生小數(shù)精度問題
首先補(bǔ)充一下小數(shù)的二進(jìn)制的計(jì)算方法:
十進(jìn)制小數(shù)轉(zhuǎn)為二進(jìn)制與整數(shù)相反,需要每次乘以?2
8.8125
o.8125*2?=?1.625??=>?1
0.625*2?=?1.25??????=>1
0.25*2?=?0.5??????????=>0
0.5*2?=?1?????????????????=>1
小數(shù)部分為?1101
二進(jìn)制小數(shù)轉(zhuǎn)為十進(jìn)制
1*2^-1?+?1*2^-2?+?0*2^-3?+?1*2^-4
在了解數(shù)字的存儲(chǔ)后,很容易理解小數(shù)精度問題,因?yàn)槭M(jìn)制有 Π 這種無限循環(huán)數(shù)字,二進(jìn)制也有循環(huán)數(shù)字。比如讓 0.1 變?yōu)槎M(jìn)制,按照二進(jìn)制轉(zhuǎn)換永遠(yuǎn)會(huì)有余數(shù),所以會(huì)是一個(gè)無限循環(huán)的二進(jìn)制 0.0001 1001 1001 1001...(1100循環(huán))。0.2 也是同理 ?0.0011 0011 0011 0011...(0011循環(huán))。
所以當(dāng)兩個(gè)浮點(diǎn)數(shù)相加時(shí),結(jié)果會(huì)有一些誤差。比如 0.1 + 0.2 ,實(shí)際上是 0.0001 1001 1001...(1001循環(huán)) + 0.0011 0011 0011...(0011循環(huán)),如果截取于第 52 位,就會(huì)得到一個(gè)有誤差的結(jié)果,轉(zhuǎn)為十進(jìn)制為0.30000000000000004,與 0.3 不相等。
3. 前端如何保證小數(shù)準(zhǔn)確
首先出于安全性及準(zhǔn)確性考慮,重要的數(shù)字計(jì)算應(yīng)該交給服務(wù)端負(fù)責(zé),相對(duì)于前端,服務(wù)端有更成熟穩(wěn)定的數(shù)字處理方法,安全性也會(huì)更高。
當(dāng)然前端有時(shí)也需要一些精確的數(shù)字計(jì)算,比如一些動(dòng)畫處理、定時(shí)器處理以及一些條件判斷等。我簡(jiǎn)單列舉幾種方法供大家參考:
toFixed 指定小數(shù)位數(shù) 這種方法比較簡(jiǎn)單,不過有個(gè)點(diǎn)要注意,這個(gè)方法是四舍五入,但有時(shí)候看上去并不會(huì),比如 2.55.toFixed(1) 顯示的結(jié)果是 2.5 而不是 2.6。這是因?yàn)?2.55 二進(jìn)制存儲(chǔ)的值并不精確,調(diào)用 2.55.toPrecision(100) 可以看到這個(gè)數(shù)的實(shí)際值是 2.5499..... ,所以截取一位四舍五入是 2.5。再舉一個(gè)例子 (2.449999999999999999).toFixed(1) = 2.5,因?yàn)檫@個(gè)數(shù)與 2.45 的差值小于 Number.EPSILON。
將小數(shù)轉(zhuǎn)為整數(shù)計(jì)算 這個(gè)方法的問題是轉(zhuǎn)換會(huì)增加額外的復(fù)雜度和計(jì)算量,在某些場(chǎng)景下,可能會(huì)導(dǎo)致數(shù)值溢出問題。
第三方庫 精確計(jì)算推薦使用成熟的庫,像 BigNumber.js、decimal.js ,進(jìn)行高精度的浮點(diǎn)數(shù)計(jì)算。原理是把數(shù)字計(jì)算變?yōu)樽址?jì)算。
JavaScript 的計(jì)算比較復(fù)雜,由于沒有細(xì)分?jǐn)?shù)字類型,底層計(jì)算以二進(jìn)制進(jìn)行,存儲(chǔ)值、計(jì)算值都有可能因?yàn)榫葋G失而不準(zhǔn)確,而顯示值可能會(huì)因?yàn)闉g覽器等宿主環(huán)境不同而有差別,所以一定要注意經(jīng)常產(chǎn)生精度丟失的地方。
-?END?-
關(guān)于奇舞團(tuán)
奇舞團(tuán)是 360 集團(tuán)最大的大前端團(tuán)隊(duì),代表集團(tuán)參與 W3C 和 ECMA 會(huì)員(TC39)工作。奇舞團(tuán)非常重視人才培養(yǎng),有工程師、講師、翻譯官、業(yè)務(wù)接口人、團(tuán)隊(duì) Leader 等多種發(fā)展方向供員工選擇,并輔以提供相應(yīng)的技術(shù)力、專業(yè)力、通用力、領(lǐng)導(dǎo)力等培訓(xùn)課程。奇舞團(tuán)以開放和求賢的心態(tài)歡迎各種優(yōu)秀人才關(guān)注和加入奇舞團(tuán)。
文章來源:http://www.zghlxwxcb.cn/news/detail-839176.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-839176.html
到了這里,關(guān)于為什么 JavaScript 中的 0.1 + 0.2 不等于 0.3的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!