該死的IEEE-754浮點(diǎn)數(shù)志鹃,說「約」就「約」曹铃,你的底線呢陕见?以JS的名義來好好查查你

IEEE 754 表示:你盡管抓狂评甜、罵娘仔涩,但你能完全避開我,算我輸柑肴。

一嘉抒、IEEE-754浮點(diǎn)數(shù)捅出的那些婁子

首先我們還是來看幾個(gè)簡(jiǎn)單的問題袍暴,能說出每一個(gè)問題的細(xì)節(jié)的話就可以跳過了政模,而如果只能泛泛說一句“因?yàn)镮EEE754浮點(diǎn)數(shù)精度問題”淋样,那么下文還是值得一看。

第一個(gè)問題是知名的0.1+0.2 != 0.3刊咳,為什么娱挨?菜鳥會(huì)告訴你“因?yàn)镮EEE 754的浮點(diǎn)數(shù)表示標(biāo)準(zhǔn)”跷坝,老鳥會(huì)補(bǔ)充道“0.1和0.2不能被二進(jìn)制浮點(diǎn)數(shù)精確表示柴钻,這個(gè)加法會(huì)使精度喪失”贴届,巨鳥會(huì)告訴你整個(gè)過程是怎樣的蜡吧,小數(shù)加法精度可能在哪幾步喪失斩跌,你能答上細(xì)節(jié)么耀鸦?

第二個(gè)問題袖订,既然十進(jìn)制0.1不能被二進(jìn)制浮點(diǎn)數(shù)精確存儲(chǔ)洛姑,那么為什么console.log(0.1)打印出來的確確實(shí)實(shí)是0.1這個(gè)精確的值楞艾?

第三個(gè)問題,你知道這些比較結(jié)果是怎么回事么蕴侧?

//這相等和不等是怎么回事净宵?
0.100000000000000002 ==
0.100000000000000010 // true

0.100000000000000002 ==
0.100000000000000020 // false

//顯然下面的數(shù)值沒有超過Number.MAX_SAFE_INTEGER的范圍择葡,為什么是這樣剃氧?
Math.pow(10, 10) + Math.pow(10, -7) === Math.pow(10, 10) //  true
Math.pow(10, 10) + Math.pow(10, -6) === Math.pow(10, 10) //  false

追問一句她我,給出一個(gè)數(shù)番舆,給這個(gè)數(shù)加一個(gè)增量,再和這個(gè)數(shù)比較疏哗,要保持結(jié)果是true返奉,即相等芽偏,那么大約這個(gè)增量的數(shù)量級(jí)最大可以到多少污尉,你能估計(jì)出來么?

第四個(gè)問題某宪,旁友锐朴,你知道下面這段一直在被引用的的代碼么(這段代碼用于解決常見范圍內(nèi)的小數(shù)加法以符合常識(shí)焚志,比如將0.1+0.2結(jié)果精確計(jì)算為0.3)娩嚼?你理解這樣做的思路么?但是你知道這段代碼有問題么佃迄?比如你計(jì)算268.34+0.83就會(huì)出現(xiàn)問題呵俏。

//注意函數(shù)接受兩個(gè)string形式的數(shù)
function numAdd(num1/*:String*/, num2/*:String*/) { 
    var baseNum, baseNum1, baseNum2; 
    try { 
        baseNum1 = num1.split(".")[1].length; 
    } catch (e) { 
        baseNum1 = 0; 
    } 
    try { 
        baseNum2 = num2.split(".")[1].length; 
    } catch (e) { 
        baseNum2 = 0;
    } 
    baseNum = Math.pow(10, Math.max(baseNum1, baseNum2)); 
    return (num1 * baseNum + num2 * baseNum) / baseNum; 
};

//看上去好像解決了0.1+0.2
numAdd("0.1","0.2"); //返回精確的0.3

//但是你試試這個(gè)
numAdd("268.34","0.83");//返回 269.16999999999996

那么多問題普碎,還真是該死的IEEE-754麻车,而這一切都源于IEEE-754浮點(diǎn)數(shù)本身的格式动猬,以及“說「約」就「約」”(舍入)的規(guī)則赁咙,致使精度喪失免钻,計(jì)算淪喪极舔,作為一個(gè)前端,我們就從JS的角度來扒一扒盯桦。

二俺附、端詳一下IEEE-754雙精度浮點(diǎn)的樣貌

所謂“知己知彼事镣,百戰(zhàn)不殆”璃哟,要從內(nèi)部瓦解敵人喊递,就要先了解敵人骚勘,但為什么只選擇雙精度呢俏讹,因?yàn)橹懒穗p精度就明白了單精度,而且在JavaScript中户矢,所有的Number都是以64-bit的雙精度浮點(diǎn)數(shù)存儲(chǔ)的梯浪,所以我們來回顧一下到底是怎么存儲(chǔ)的挂洛,以及這樣子存儲(chǔ)怎么映射到具體的數(shù)值抹锄。

IEEE754浮點(diǎn)數(shù)形式
IEEE754浮點(diǎn)數(shù)形式

二進(jìn)制在存儲(chǔ)的時(shí)候是以二進(jìn)制的“科學(xué)計(jì)數(shù)法”來存儲(chǔ)的伙单,我們回顧下十進(jìn)制的科學(xué)計(jì)數(shù)法哈肖,比如54846.3淤井,這個(gè)數(shù)我們?cè)谟脴?biāo)準(zhǔn)的科學(xué)計(jì)數(shù)法應(yīng)該是這樣的:5.48463e4,這里有三部分游两,第一是符號(hào)贱案,這是一個(gè)正數(shù),只是一般省略正號(hào)不寫侨糟,第二是有效數(shù)字部分秕重,這里就是5.48463溶耘,最后是指數(shù)部分服鹅,這里是4菱魔。以上就是在十進(jìn)制領(lǐng)域下的科學(xué)計(jì)數(shù)法澜倦,換到二進(jìn)制也是一樣,只是十進(jìn)制下以10為底碘勉,二進(jìn)制以2為底验靡。

雙精度的浮點(diǎn)數(shù)在這64位上劃分為3段胜嗓,而這3段也就確定了一個(gè)浮點(diǎn)數(shù)的值辞州,64bit的劃分是“1-11-52”的模式寥粹,具體來說:

  • 就是1位最高位(最左邊那一位)表示符號(hào)位,0表示正媚狰,1表示負(fù)
  • 接下去11位表示指數(shù)部分
  • 最后52位表示尾數(shù)部分崭孤,也就是有效域部分

這里幺蛾子就很多了裳瘪。首先“每個(gè)實(shí)數(shù)都有一個(gè)相反數(shù)”這是中學(xué)教的彭羹,于是符號(hào)位改變下就是一個(gè)相反數(shù)了派殷,但是對(duì)于數(shù)字0來說毡惜,相反數(shù)就是自己斯撮,而符號(hào)位對(duì)于每一個(gè)由指數(shù)域和尾數(shù)域確定的數(shù)都是一視同仁勿锅,有正就有負(fù),要么都沒有垮刹。所以這里就有正0和負(fù)0的概念荒典,但是正0和負(fù)0是相等的寺董,但是他們能反應(yīng)出符號(hào)位的不同螃征,和正零透敌、負(fù)零相關(guān)的有意思的事這里不贅述。

然后魄藕,指數(shù)不一定要正數(shù)吧背率,可以是負(fù)數(shù)吧,一種方式是指數(shù)域部分也設(shè)置一個(gè)符號(hào)位交排,第二種是IEEE754采取的方式埃篓,設(shè)置一個(gè)偏移架专,使指數(shù)部分永遠(yuǎn)表現(xiàn)為一個(gè)非負(fù)數(shù)部脚,然后減去某個(gè)偏移值才是真實(shí)的指數(shù)裤纹,這樣做的好處是可以表現(xiàn)一些極端值,我們等會(huì)會(huì)看到鹰椒。而64bit的浮點(diǎn)數(shù)設(shè)置的偏移值是1023吹零,因?yàn)橹笖?shù)域表現(xiàn)為一個(gè)非負(fù)數(shù)灿椅,11位,所以 0 <= e <= 2^11 -1操刀,實(shí)際的E=e-1023骨坑,所以 -1023 <= E <= 1024欢唾。這兩端的兩個(gè)極端值結(jié)合不同的尾數(shù)部分代表了不同的含義礁遣。

最后祟霍,尾數(shù)部分,也就是有效域部分醇王,為什么叫有效域部分寓娩,舉個(gè)栗子,這里有52個(gè)坑力试,但是你的數(shù)字由60個(gè)二進(jìn)制1組成畸裳,不管怎樣,你都是不能完全放下的帅容,只能放下52個(gè)1并徘,那剩下的8個(gè)1呢麦乞?要么舍入要么舍棄了姐直,總之是無效了蒋畜。所以姻成,尾數(shù)部分決定了這個(gè)數(shù)的精度。

而對(duì)于二進(jìn)制的科學(xué)計(jì)數(shù)法辫狼,如果保持小數(shù)點(diǎn)前必須有一位非0的膨处,那有效域是不是必然是1.XXXX的形式真椿?而這樣子的二進(jìn)制被稱為規(guī)格化的,這樣的二進(jìn)制在存儲(chǔ)時(shí)测摔,小數(shù)點(diǎn)前的1是默認(rèn)存在锋八,但是默認(rèn)不占坑的挟纱,尾數(shù)部分就存儲(chǔ)小數(shù)點(diǎn)后的部分紊服。

問題來了胸竞,如果這個(gè)二進(jìn)制小數(shù)太小了卫枝,那么會(huì)出現(xiàn)什么情況呢?對(duì)于一個(gè)接近于0的二進(jìn)制小數(shù)腺占,一味追求1.xxx的形式衰伯,必然導(dǎo)致指數(shù)部分會(huì)向負(fù)無窮靠攏意鲸,而真實(shí)的指數(shù)部分最小也就能表示-1023怎顾,一旦把指數(shù)部分逼到了-1023,還沒有到1.xxx的形式夭委,那么只能用0.xxx的形式表示有效部分株灸,這樣的二進(jìn)制浮點(diǎn)數(shù)表示非規(guī)格化的慌烧。

于是屹蚊,我們整一個(gè)64位浮點(diǎn)數(shù)能表示的值由符號(hào)位s汹粤,指數(shù)域e和尾數(shù)域f確定如下田晚,從中我們可以看到正負(fù)零肉瓦、規(guī)格化和非規(guī)格化二進(jìn)制浮點(diǎn)數(shù)胃惜、正負(fù)無窮是怎么表示的:

浮點(diǎn)數(shù)形式和數(shù)值的映射
浮點(diǎn)數(shù)形式和數(shù)值的映射

這里的(0.f)(1.f)指的是二進(jìn)制的表示船殉,都要轉(zhuǎn)化為十進(jìn)制再去計(jì)算利虫,這樣你就可以得到最終值糠惫。

回顧了IEEE754的64bit浮點(diǎn)數(shù)之后,有以下3點(diǎn)需要牢記的:

  1. 指數(shù)和尾數(shù)域是有限的巢价,一個(gè)是11位壤躲,一個(gè)是52位
  2. 符號(hào)位決定正負(fù),指數(shù)域決定數(shù)量級(jí)凌唬,尾數(shù)域決定精度
  3. 所有數(shù)值的計(jì)算和比較客税,都是這樣以64個(gè)bit的形式來進(jìn)行的霎挟,拋開腦海中想當(dāng)然的十進(jìn)制

三酥夭、精度在哪里發(fā)生丟失

當(dāng)你直接計(jì)算0.1+0.2時(shí)脊奋,你要知道“你大媽已經(jīng)不是你大媽诚隙,你大爺也已經(jīng)不是你大爺了久又,所以他們生的孩子(結(jié)果)出現(xiàn)問題就可以理解了”地消。這里的0.10.2是十進(jìn)制下的0.1和0.2,當(dāng)它們轉(zhuǎn)化為二進(jìn)制時(shí)疼阔,它們是無限循環(huán)的二進(jìn)制表示婆廊。

這引出第一處可能丟失精度的地方淘邻,即在十進(jìn)制轉(zhuǎn)二進(jìn)制的過程中丟失精度宾舅。因?yàn)榇蟛糠值氖M(jìn)制小數(shù)是不能被這52位尾數(shù)的二進(jìn)制小數(shù)表示完畢的,我們眼中最簡(jiǎn)單的0.1砂吞、0.2在轉(zhuǎn)化為二進(jìn)制小數(shù)時(shí)都是無限循環(huán)的蜻直,還有些可能不是無限循環(huán)的袁串,但是轉(zhuǎn)化為二進(jìn)制小數(shù)的時(shí)候囱修,小數(shù)部分超過了52位破镰,那也是放不下的。

那么既然只有52位的有效域源譬,那么必然超出52位的部分會(huì)發(fā)生一件靈異事件——閹割踩娘,文明點(diǎn)叫“舍入”养渴。IEEE754規(guī)定了幾種舍入規(guī)則理卑,但是默認(rèn)的是舍入到最接近的值胶惰,如果“舍”和“入”一樣接近孵滞,那么取結(jié)果為偶數(shù)的選擇坊饶。

所以上面的0.1+0.2中殴蓬,當(dāng)0.1和0.2被存儲(chǔ)時(shí),存進(jìn)去的已經(jīng)不是精確的0.1和0.2了痘绎,而是精度發(fā)生一定丟失的值孤页。但是精度丟失還沒有完行施,當(dāng)這個(gè)兩個(gè)值發(fā)生相加時(shí)蛾号,精度還可能進(jìn)一步丟失,注意幾次精度丟失的疊加不一定使結(jié)果偏差越來越大哦展运。

第二處可能丟失精度的地方是浮點(diǎn)數(shù)參與計(jì)算時(shí)乐疆,浮點(diǎn)數(shù)參與計(jì)算時(shí)挤土,有一個(gè)步驟叫對(duì)階仰美,以加法為例儿礼,要把小的指數(shù)域轉(zhuǎn)化為大的指數(shù)域蚊夫,也就是左移小指數(shù)浮點(diǎn)數(shù)的小數(shù)點(diǎn)知纷,一旦小數(shù)點(diǎn)左移,必然會(huì)把52位有效域的最右邊的位給擠出去伍绳,這個(gè)時(shí)候擠出去的部分也會(huì)發(fā)生“舍入”冲杀。這就又會(huì)發(fā)生一次精度丟失。

所以就0.1+0.2這個(gè)例子精度在兩個(gè)數(shù)轉(zhuǎn)為二進(jìn)制過程中和相加過程中都已經(jīng)丟失了精度剩檀,那么最后的結(jié)果有問題谨朝,不能如愿也就不奇怪了字币,如果你很想探究具體這是怎么計(jì)算的洗出,文末附錄的鏈接能幫助你图谷。

四便贵、疑惑:0.1不能被精確表示,但打印0.1它就是0.1啊

是的利耍,照理說隘梨,0.1不能被精確表示舷嗡,存儲(chǔ)的是0.1的一個(gè)近似值,那么我打印0.1時(shí)进萄,比如console.log(0.1)中鼠,就是打印出了精確的0.1啊。

事實(shí)是扰肌,當(dāng)你打印的時(shí)候熊杨,其實(shí)發(fā)生了二進(jìn)制轉(zhuǎn)為十進(jìn)制晶府,十進(jìn)制轉(zhuǎn)為字符串,最后輸出的剂习。而十進(jìn)制轉(zhuǎn)為二進(jìn)制會(huì)發(fā)生近似鳞绕,那么二進(jìn)制轉(zhuǎn)為十進(jìn)制也會(huì)發(fā)生近似们何,打印出來的值其實(shí)是近似過的值控轿,并不是對(duì)浮點(diǎn)數(shù)存儲(chǔ)內(nèi)容的精確反映。

關(guān)于這個(gè)問題鹦蠕,StackOverflow上有一個(gè)回答可以參考钟病,回答中指出了一篇文獻(xiàn)档悠,有興趣的可以去看:

How does javascript print 0.1 with such accuracy?

五望浩、相等不相等磨德,就看這64個(gè)bit

再次強(qiáng)調(diào)典挑,所有數(shù)值的計(jì)算和比較,都是這樣以64個(gè)bit的形式來進(jìn)行的拙寡,當(dāng)這64個(gè)bit容不下時(shí)肆糕,就會(huì)發(fā)生近似,一近似就發(fā)生意外了淮摔。

有一些在線的小數(shù)轉(zhuǎn)IEEE754浮點(diǎn)數(shù)的應(yīng)用對(duì)于驗(yàn)證一些結(jié)果還是很有幫助的和橙,你可以用這個(gè)IEEE-754 Floating-Point Conversion工具幫你驗(yàn)證你的小數(shù)轉(zhuǎn)化為IEEE754浮點(diǎn)數(shù)之后是怎么個(gè)鬼樣魔招。

來看第一部分中提出兩個(gè)簡(jiǎn)單的比較問題:

//這相等和不等是怎么回事仆百?
0.100000000000000002 ==
0.1  //true

0.100000000000000002 ==
0.100000000000000010 // true

0.100000000000000002 ==
0.100000000000000020 // false

當(dāng)你把0.1奔脐、0.100000000000000002髓迎、0.100000000000000010.10000000000000002用上面的工具轉(zhuǎn)為浮點(diǎn)數(shù)后,你會(huì)發(fā)現(xiàn)波势,他們的尾數(shù)部分(注意看尾數(shù)部分最低4位橄维,其余位都是相同的)争舞,前三個(gè)是相同的竞川,最低4位是1010委乌,但是最后一個(gè)轉(zhuǎn)化為浮點(diǎn)數(shù)尾數(shù)最低4位是1011。

這是因?yàn)樗鼈冊(cè)谵D(zhuǎn)為二進(jìn)制時(shí)要舍入部分的不同可能造成的不同舍入導(dǎo)致在尾數(shù)上可能呈現(xiàn)不一致戈咳,而比較兩個(gè)數(shù)著蛙,本質(zhì)上是比較這兩個(gè)數(shù)的這64個(gè)bit册踩,不同即是不等的暂吉,有一個(gè)例外慕的,+0==-0挤渔。

再來看提到的第二個(gè)相等問題:

Math.pow(10, 10) + Math.pow(10, -7) === Math.pow(10, 10) //  true
Math.pow(10, 10) + Math.pow(10, -6) === Math.pow(10, 10) //  false

為什么上面一個(gè)是可以相等的判导,下面一個(gè)就不行了绕辖,首先我們來轉(zhuǎn)化下:

Math.pow(10, 10) =>
指數(shù)域 e =1056 仪际,即 E = 33
尾數(shù)域 (1.)0010101000000101111100100000000000000000000000000000

Math.pow(10, -7) =>
指數(shù)域 e =999 昵骤,即 E = -24

Math.pow(10, -6) =>
指數(shù)域 e =1003 变秦,即 E = -20
尾數(shù)域 (1.)0000110001101111011110100000101101011110110110001101

可以看到1e10的指數(shù)是33次蹦玫,而Math.pow(10, -7)指數(shù)是-24次钳垮,相差57次,遠(yuǎn)大于52歧焦,因此绢馍,相加時(shí)發(fā)生對(duì)階舰涌,早就把Math.pow(10, -7)近似成0了朱躺。

Math.pow(10, -6)指數(shù)是-20次搁痛,相差53次鸡典,看上去大于52次彻况,但有一個(gè)默認(rèn)的前導(dǎo)1別忘了纽甘,于是當(dāng)發(fā)生對(duì)階贷腕,小數(shù)點(diǎn)左移53位時(shí)泽裳,這一串尾數(shù)(別忘了前導(dǎo)1)正好被擠出第52位涮总,這時(shí)候就會(huì)發(fā)生”舍入“瀑梗,舍入結(jié)果是最低位谤职,也就是bit0位變成1亿鲜,這個(gè)時(shí)候和Math.pow(10, 10)相加饶套,結(jié)果的最低位變成了1妓蛮,自然和Math.pow(10, 10)不相等。

你可以用這個(gè)IEEE754計(jì)算器來驗(yàn)證結(jié)果蛤克。

六捺癞、淺析數(shù)值和數(shù)值精度的數(shù)量級(jí)對(duì)應(yīng)關(guān)系

承接上面的那個(gè)結(jié)果,我們發(fā)現(xiàn)當(dāng)數(shù)值為10的10次時(shí)咖耘,加一個(gè)-7數(shù)量級(jí)的數(shù)翘簇,對(duì)于值沒有影響撬码,加一個(gè)-6數(shù)量級(jí)的數(shù)儿倒,卻對(duì)值由影響,這里的本質(zhì)我們也是知道的:

這是由于計(jì)算時(shí)要對(duì)階呜笑,如果一個(gè)小的增量在對(duì)階時(shí)最高有效位右移(因?yàn)樾?shù)點(diǎn)在左移)到了52位開外夫否,那么這個(gè)增量就很可能被忽略,即對(duì)階完尾數(shù)被近似成0微谓。

換句話說,我們可以說對(duì)于1010數(shù)量級(jí),其精確度大約在10-6數(shù)量級(jí)肴焊,那么對(duì)于109、108、100等等數(shù)量級(jí)的值席揽,精確度又大約在多少呢?

有一張圖很好地說明了這個(gè)對(duì)應(yīng)關(guān)系:

數(shù)值數(shù)量級(jí)和精確度數(shù)量級(jí)對(duì)應(yīng)關(guān)系
數(shù)值數(shù)量級(jí)和精確度數(shù)量級(jí)對(duì)應(yīng)關(guān)系

這張圖熊痴,橫坐標(biāo)表示浮點(diǎn)數(shù)值數(shù)量級(jí)系谐,縱坐標(biāo)表示可以到達(dá)的精度的數(shù)量級(jí)鄙煤,當(dāng)然這里橫坐標(biāo)對(duì)應(yīng)的數(shù)值數(shù)量級(jí)指的是十進(jìn)制表示下的數(shù)量級(jí)薪寓。

比如你在控制臺(tái)測(cè)試(.toFixed()函數(shù)接受一個(gè)20及以內(nèi)的整數(shù)n以顯示小數(shù)點(diǎn)后n位):

0.1.toFixed(20) ==> 0.10000000000000000555(這里也可以看出0.1是精確存儲(chǔ)的),根據(jù)上面的圖我們知道0.1是10-1數(shù)量級(jí)的旷太,那么精確度大約在10-17左右,而我們驗(yàn)證一下:

//動(dòng)10的-18數(shù)量級(jí)及之后的數(shù)字,并不會(huì)有什么演顾,依舊判定相等
0.10000000000000000555 ==
0.10000000000000000999  //true
//動(dòng)10的-17數(shù)量級(jí)上的數(shù)字,結(jié)果馬上不一樣了
0.10000000000000000555 ==
0.10000000000000001555  //false

從圖上也可以看到之前的那個(gè)例子屿脐,1010數(shù)量級(jí),精確度在10-6數(shù)量級(jí)。

也就是說代赁,在IEEE754的64位浮點(diǎn)數(shù)表示下禾进,如果一個(gè)數(shù)的數(shù)量級(jí)在10X,其精確度在10Y,那么X和Y大致滿足:

X-16=Y

知道這個(gè)之后我們?cè)倩剡^頭來看ECMA在定義的Number.EPSILON,如果還不知道有這個(gè)的存在廉白,可以控制臺(tái)去輸出下楣嘁,這個(gè)數(shù)大約是10-16數(shù)量級(jí)的一個(gè)數(shù)谆膳,這個(gè)數(shù)定義為”大于1的能用IEEE754浮點(diǎn)數(shù)表示為數(shù)值的最小數(shù)與1的差值“,這個(gè)數(shù)用來干嘛呢?

0.1+0.2-0.3<Number.EPSILON是返回true的,也就是說ECMA預(yù)設(shè)了一個(gè)精度,便于開發(fā)者使用沿量,但是我們現(xiàn)在可以知道這個(gè)預(yù)定義的值其實(shí)是對(duì)應(yīng) 100 數(shù)量級(jí)數(shù)值的精確度钓简,如果你要比較更小數(shù)量級(jí)的兩個(gè)數(shù),預(yù)定義的這個(gè)Number.EPSILON就不夠用了(不夠精確了)侦啸,你可以用數(shù)學(xué)方式將這個(gè)預(yù)定義值的數(shù)量級(jí)進(jìn)行縮小顶捷。

七、麻煩稍小的整數(shù)提供一種解決思路

那么怎樣能在計(jì)算機(jī)中實(shí)現(xiàn)看上去比較正常和自然的小數(shù)計(jì)算呢?比如0.1+0.2就輸出0.3。其中一個(gè)思路命爬,也是目前足夠應(yīng)付大多數(shù)場(chǎng)景的思路就是艇抠,將小數(shù)轉(zhuǎn)化為整數(shù)絮重,在整數(shù)范圍內(nèi)計(jì)算結(jié)果暂氯,再把結(jié)果轉(zhuǎn)化為小數(shù)辣吃,因?yàn)?strong>存在一個(gè)范圍哩簿,這個(gè)范圍內(nèi)的整數(shù)是可以被IEEE754浮點(diǎn)形式精確表示的,換句話說這個(gè)范圍內(nèi)的整數(shù)運(yùn)算,結(jié)果都是精確的让歼,而大部分場(chǎng)景下這個(gè)數(shù)的范圍已經(jīng)夠用倚评,所以這種思路可行呢岗。

1. JS中數(shù)的“量程”和“精度”

之所以說一個(gè)范圍挫酿,而不是所有的整數(shù)葱弟,是因?yàn)檎麛?shù)也存在精確度的問題藏杖,要深刻地理解,”可表示范圍“和”精確度“兩個(gè)概念的區(qū)別,就像一把尺子的”量程“和”精度“兽间。

JS所能表示的數(shù)的范圍帜羊,以及能表示的安全整數(shù)范圍(安全是指不損失精確度)由以下幾個(gè)值界定:

//自己可以控制臺(tái)打印看看
Number.MAX_VALUE => 能表示的最大正數(shù)饥瓷,數(shù)量級(jí)在10的308次
Number.MIN_VALUE => 能表示的最小正數(shù)刺洒,注意不是最小數(shù),最小數(shù)是上面那個(gè)取反抹剩,10的-324數(shù)量級(jí)

Number.MAX_SAFE_INTEGER => 能表示的最大安全數(shù)钳踊,9開頭的16位數(shù)
Number.MIN_SAFE_INTEGER => 能表示的最小安全數(shù)祭埂,上面那個(gè)的相反數(shù)

為什么超過最大安全數(shù)的整數(shù)都不精確了呢?還是回到IEEE754的那幾個(gè)坑上,尾數(shù)就52個(gè)坑,有效數(shù)再多,就要發(fā)生舍入了评架。

2. 一段有瑕疵的解決浮點(diǎn)計(jì)算異常問題的代碼

因此籽腕,回到解決JS浮點(diǎn)數(shù)的精確計(jì)算上來郎楼,可以把待計(jì)算的小數(shù)轉(zhuǎn)化為整數(shù),在安全整數(shù)范圍內(nèi)珍策,再計(jì)算結(jié)果疗绣,再轉(zhuǎn)回小數(shù)塔逃。

所以有了下面這段代碼(但這是有問題的):

//注意要傳入兩個(gè)小數(shù)的字符串表示丙挽,不然在小數(shù)轉(zhuǎn)成二進(jìn)制浮點(diǎn)數(shù)的過程中精度就已經(jīng)損失了
function numAdd(num1/*:String*/, num2/*:String*/) { 
    var baseNum, baseNum1, baseNum2; 
    try { 
        //取得第一個(gè)操作數(shù)小數(shù)點(diǎn)后有幾位數(shù)字,注意這里的num1是字符串形式的
        baseNum1 = num1.split(".")[1].length; 
    } catch (e) {
        //沒有小數(shù)點(diǎn)就設(shè)為0 
        baseNum1 = 0; 
    } 
    try { 
        //取得第二個(gè)操作數(shù)小數(shù)點(diǎn)后有幾位數(shù)字
        baseNum2 = num2.split(".")[1].length; 
    } catch (e) { 
        baseNum2 = 0;
    }
    //計(jì)算需要 乘上多少數(shù)量級(jí) 才能把小數(shù)轉(zhuǎn)化為整數(shù) 
    baseNum = Math.pow(10, Math.max(baseNum1, baseNum2)); 
    //把兩個(gè)操作數(shù)先乘上計(jì)算所得數(shù)量級(jí)轉(zhuǎn)化為整數(shù)再計(jì)算均蜜,結(jié)果再除以這個(gè)數(shù)量級(jí)轉(zhuǎn)回小數(shù)
    return (num1 * baseNum + num2 * baseNum) / baseNum; 
};

思路沒有問題匪蟀,看上去也解決了0.1+0.2的問題嘁捷,用上面的函數(shù)計(jì)算numAdd("0.1","0.2")時(shí)履肃,輸出確實(shí)是0.3成福。但是再多試幾個(gè),比如numAdd("268.34","0.83")忽冻,輸出是269.16999999999996僧诚,瞬間爆炸,這些代碼一行都不想再看秀菱。

其實(shí)仔細(xì)分析一下,這個(gè)問題還是很好解決的琼锋。問題是這么發(fā)生的,有一個(gè)隱式的類型轉(zhuǎn)換,上面的num1和num2傳入都是字符串類型的,但是在最后return的那個(gè)表達(dá)式中袁稽,直接參與計(jì)算诊胞,于是num1和num2隱式地從String轉(zhuǎn)為Number裕菠,而Number是以IEEE754浮點(diǎn)數(shù)形式儲(chǔ)存的平委,在十進(jìn)制轉(zhuǎn)為二進(jìn)制過程中,精度會(huì)損失笆环。

我們可以在上面代碼的return語句之上加上這兩句看看輸出是什么:

console.log(num1 * baseNum);
console.log(num2 * baseNum);

你會(huì)發(fā)現(xiàn)針對(duì)numAdd("268.34","0.83")的例子鳖擒,上面兩行輸出26833.999999999996互躬、83」⑵荩可以看到轉(zhuǎn)化為整數(shù)的夢(mèng)想并沒有被很好地實(shí)現(xiàn)

要解決這個(gè)問題也很容易呜呐,就是我們顯式地讓小數(shù)“乖乖”轉(zhuǎn)為整數(shù),因?yàn)槲覀冎纼蓚€(gè)操作數(shù)乘上計(jì)算所得數(shù)量級(jí)必然應(yīng)該是一個(gè)整數(shù)膀懈,只是由于精度損失放大導(dǎo)致被近似成了一個(gè)小數(shù),那我們把結(jié)果保留到整數(shù)部分不就可以了么撼短?

也就是把上面最后一句的

return (num1 * baseNum + num2 * baseNum) / baseNum;
改為
return (num1 * baseNum + num2 * baseNum).toFixed(0) / baseNum;

分子上的.toFixed(0)表示精確到整數(shù)位艳吠,這基于我們明確地知道分子是一個(gè)整數(shù)迈嘹。

3. 局限性和其他可能的思路

這種方式的局限性在于我要乘上一個(gè)數(shù)量級(jí)把小數(shù)轉(zhuǎn)為整數(shù)沛励,如果小數(shù)部分很長(zhǎng)呢谅摄,那么通過這個(gè)方式轉(zhuǎn)化出的整數(shù)就超過了安全整數(shù)的范圍,那么計(jì)算也就不安全了土辩。

不過還是一句話黎做,看使用場(chǎng)景進(jìn)行選擇楣铁,如果局限性不會(huì)出現(xiàn)或者出現(xiàn)了但是無傷大雅补鼻,那就可以應(yīng)用刊殉。

另一種思路是將小數(shù)轉(zhuǎn)為字符串肛响,用字符串去模擬角塑,這樣子做可適用的范圍比較廣,但是實(shí)現(xiàn)過程會(huì)比較繁瑣欺劳。

如果你的項(xiàng)目中需要多次面臨這樣的計(jì)算枫弟,又不想自己實(shí)現(xiàn)绪爸,那么也有現(xiàn)成的庫可以使用,比如math.js邓馒,感謝這個(gè)美好的世界吧。

八视事、小結(jié)

作為一個(gè)JS程序員,IEEE754浮點(diǎn)數(shù)可能不會(huì)經(jīng)常讓你心煩庆揩,但是明白這些能讓你在以后遇到相關(guān)意外時(shí)保持冷靜俐东,正常看待订晌÷脖瑁看完全文,我們應(yīng)該能明白IEEE754的64位浮點(diǎn)數(shù)表示方式和對(duì)應(yīng)的值锈拨,能明白精度和范圍的區(qū)別砌庄,能明白精度損失、意外的比較結(jié)果都是源自于那有限數(shù)量的bit,而不用每次遇到類似問題就發(fā)一個(gè)日經(jīng)的問題娄昆,不會(huì)就知道“IEEE754”這一個(gè)詞的皮毛卻說不出一句完整的表達(dá)佩微,最重要是能夠心平氣和地罵一句“你這該死的IEEE754”后繼續(xù)coding...

如有紕漏煩請(qǐng)留言指出,謝謝萌焰。

附:感謝以下內(nèi)容對(duì)我的幫助

實(shí)現(xiàn)js浮點(diǎn)數(shù)加哺眯、減、乘扒俯、除的精確計(jì)算
IEEE-754 Floating-Point Conversion IEEE-754浮點(diǎn)數(shù)轉(zhuǎn)換工具
IEEE754 浮點(diǎn)數(shù)格式 與 Javascript number 的特性
Number.EPSILON及其它屬性

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末奶卓,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子撼玄,更是在濱河造成了極大的恐慌夺姑,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件掌猛,死亡現(xiàn)場(chǎng)離奇詭異盏浙,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)留潦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門只盹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人兔院,你說我怎么就攤上這事殖卑。” “怎么了坊萝?”我有些...
    開封第一講書人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵孵稽,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我十偶,道長(zhǎng)菩鲜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任惦积,我火速辦了婚禮接校,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘狮崩。我一直安慰自己蛛勉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開白布睦柴。 她就那樣靜靜地躺著诽凌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪坦敌。 梳的紋絲不亂的頭發(fā)上侣诵,一...
    開封第一講書人閱讀 48,970評(píng)論 1 284
  • 那天痢法,我揣著相機(jī)與錄音,去河邊找鬼杜顺。 笑死财搁,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的哑舒。 我是一名探鬼主播妇拯,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼桦卒,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼饶囚!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起可训,我...
    開封第一講書人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤膘滨,失蹤者是張志新(化名)和其女友劉穎甘凭,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體火邓,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡丹弱,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了铲咨。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片躲胳。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖纤勒,靈堂內(nèi)的尸體忽然破棺而出坯苹,到底是詐尸還是另有隱情,我是刑警寧澤摇天,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布粹湃,位于F島的核電站,受9級(jí)特大地震影響泉坐,放射性物質(zhì)發(fā)生泄漏为鳄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一腕让、第九天 我趴在偏房一處隱蔽的房頂上張望孤钦。 院中可真熱鬧,春花似錦纯丸、人聲如沸司训。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至勾徽,卻和暖如春滑凉,著一層夾襖步出監(jiān)牢的瞬間统扳,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來泰國打工畅姊, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留咒钟,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓若未,卻偏偏與公主長(zhǎng)得像朱嘴,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子粗合,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容