案例
小明是一位前端新人蔚袍,經(jīng)過(guò)大學(xué)期間的苦學(xué)勤練終于出山,就職某公司負(fù)責(zé)某電商平臺(tái)捣染。經(jīng)過(guò)一年多的項(xiàng)目實(shí)戰(zhàn)艳吠,小明碼代碼漸臻佳境衙四,發(fā)現(xiàn)JS也:
然而有一天,測(cè)試小力找到小明,告訴小明“你寫(xiě)的秒殺活動(dòng)價(jià)格有bug”。小明心里咯噔一下:我寫(xiě)的代碼怎么會(huì)有bug五督,是你測(cè)試姿勢(shì)有問(wèn)題吧?想歸想瓶殃,嘴里還是應(yīng)下來(lái)先看一下充包。根據(jù)小力提供的測(cè)試案例,小明很快就重現(xiàn)了問(wèn)題:價(jià)格為1.855的秒殺商品遥椿,在前端toFixed后竟然變成了1.85误证。經(jīng)過(guò)一番搜索,小明把bug給解決了修壕,也知道這是JS的精度缺失導(dǎo)致的問(wèn)題。那么遏考,問(wèn)題來(lái)了:是什么導(dǎo)致的精度缺失慈鸠?怎么解決精度缺失問(wèn)題?
- 常見(jiàn)的JS精度缺失場(chǎng)景:
1.855.toFixed(2) // 1.85
0.1 + 0.2 // 0.30000000000000004
9999999999999999 == 10000000000000001 // true
什么導(dǎo)致的精度缺失
在JavaScript中灌具,Number類型是采用IEEE754二進(jìn)制浮點(diǎn)數(shù)算術(shù)標(biāo)準(zhǔn)的雙精度64位浮點(diǎn)數(shù)來(lái)存儲(chǔ)的青团,運(yùn)算也是基于二進(jìn)制來(lái)運(yùn)算,最終才轉(zhuǎn)換成十進(jìn)制的結(jié)果咖楣。由于位數(shù)限制(64bit)督笆,一些不能有窮表示的二進(jìn)制數(shù)只能是真正值的近似,經(jīng)過(guò)運(yùn)算后偏差疊加诱贿,會(huì)出現(xiàn)一些意料之外的結(jié)果娃肿,也就是我們所說(shuō)的精度缺失咕缎。
- 64bit組成:
sign | exponent | fraction |
---|---|---|
符號(hào)位 | 指數(shù)域 | 尾數(shù)域 |
第1位 | 2~12位 | 13~64位 |
轉(zhuǎn)換二進(jìn)制
以上面0.1與0.2為例,在十進(jìn)制里面可以有窮表示料扰,但是轉(zhuǎn)換成二進(jìn)制后凭豪,它們就是無(wú)窮的。具體轉(zhuǎn)換方式可以遵循“正整數(shù)除二取余晒杈,小數(shù)乘二取整”的方式:
// 0.1轉(zhuǎn)二進(jìn)制
0.1*2=0.2****************取出整數(shù)部分0
0.2*2=0.4****************取出整數(shù)部分0
0.4*2=0.8****************取出整數(shù)部分0
0.8*2=1.6****************取出整數(shù)部分1
0.6*2=1.2****************取出整數(shù)部分1
0.2*2=0.4****************取出整數(shù)部分0
0.4*2=0.8****************取出整數(shù)部分0
0.8*2=1.6****************取出整數(shù)部分1
0.6*2=1.2****************取出整數(shù)部分1
.
.
.
// 結(jié)果 0.1D = 00011001100110011...B
// 科學(xué)計(jì)數(shù)法表示: 2^-4*1.100110011...B
// 尾數(shù)域52 位嫂伞,超過(guò)部分進(jìn)一舍零:2^-4*1.10011(0011 repeat 12 times)010B
最后結(jié)果:2^-4*1.1001100110011001100110011001100110011001100110011010B
同理可以得出0.2的二進(jìn)制:
0.2D = 2^-3*1.1001100110011001100110011001100110011001100110011010B
0.1 + 0.2:
2^-4*1.1001100110011001100110011001100110011001100110011010B
+ 2^-3*1.1001100110011001100110011001100110011001100110011010B
相加時(shí)指數(shù)不一致,需要對(duì)齊:
0.1100110011001100110011001100110011001100110011001101B
+ 1.1001100110011001100110011001100110011001100110011010B
--------------------------------------------------------------
=10.0110011001100110011001100110011001100110011001100111B
得到的二進(jìn)制結(jié)果為:10.0110011001100110011001100110011001100110011001100111B
重新用科學(xué)計(jì)數(shù)法表示:2^-2*1.0011001100110011001100110011001100110011001100110100
最終轉(zhuǎn)化為十進(jìn)制的數(shù)值:
let b = '010011001100110011001100110011001100110011001100110100';
let num = 0;
for (let i = 0, len = b.length; i < len; i++) {
num += b[i] * Math.pow(2, -i-1);
}
num; // 0.30000000000000004
精度缺失解決
這里介紹一個(gè)第三方庫(kù)decimal.js拯钻,項(xiàng)目里面可以進(jìn)行二次封裝使用帖努。
彩蛋
ES6 在Number對(duì)象上新增了一個(gè)極小的常量Number.EPSILON,表示JavaScript的最小精度粪般,誤差如果小于這個(gè)值拼余,就可以把二者看成是相等的。例如:
0.1 + 0.2 === 0.3 // false
0.1 + 0.2 - 0.3 < Number.EPSILON // true
然而這個(gè)也不是無(wú)往而不利刊驴,1.1 + 1.3 - 2.4 < Number.EPSILON
返回的結(jié)果再次出乎意料姿搜,是false±υ鳎看到這里舅柜,小明默默把簡(jiǎn)歷里的“精通JS”改成了“了解”。躲惰。