摘要:而的浮點(diǎn)數(shù)設(shè)置的偏移值是,因?yàn)橹笖?shù)域表現(xiàn)為一個(gè)非負(fù)數(shù),位,所以,實(shí)際的,所以。這是因?yàn)樗鼈冊(cè)谵D(zhuǎn)為二進(jìn)制時(shí)要舍入部分的不同可能造成的不同舍
一、IEEE-754浮點(diǎn)數(shù)捅出的那些婁子IEEE 754 表示:你盡管抓狂、罵娘,但你能完全避開我,算我輸。
首先我們還是來(lái)看幾個(gè)簡(jiǎn)單的問(wèn)題,能說(shuō)出每一個(gè)問(wèn)題的細(xì)節(jié)的話就可以跳過(guò)了,而如果只能泛泛說(shuō)一句“因?yàn)镮EEE754浮點(diǎn)數(shù)精度問(wèn)題”,那么下文還是值得一看。
第一個(gè)問(wèn)題是知名的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è)過(guò)程是怎樣的,小數(shù)加法精度可能在哪幾步喪失,你能答上細(xì)節(jié)么?
第二個(gè)問(wèn)題,既然十進(jìn)制0.1不能被二進(jìn)制浮點(diǎn)數(shù)精確存儲(chǔ),那么為什么console.log(0.1)打印出來(lái)的確確實(shí)實(shí)是0.1這個(gè)精確的值?
第三個(gè)問(wèn)題,你知道這些比較結(jié)果是怎么回事么?
//這相等和不等是怎么回事? 0.100000000000000002 == 0.100000000000000010 // true 0.100000000000000002 == 0.100000000000000020 // false //顯然下面的數(shù)值沒有超過(guò)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
追問(wèn)一句,給出一個(gè)數(shù),給這個(gè)數(shù)加一個(gè)增量,再和這個(gè)數(shù)比較,要保持結(jié)果是true,即相等,那么大約這個(gè)增量的數(shù)量級(jí)最大可以到多少,你能估計(jì)出來(lái)么?
第四個(gè)問(wèn)題,旁友,你知道下面這段一直在被引用的的代碼么(這段代碼用于解決常見范圍內(nèi)的小數(shù)加法以符合常識(shí),比如將0.1+0.2結(jié)果精確計(jì)算為0.3)?你理解這樣做的思路么?但是你知道這段代碼有問(wèn)題么?比如你計(jì)算268.34+0.83就會(huì)出現(xiàn)問(wè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
那么多問(wèn)題,還真是該死的IEEE-754,而這一切都源于IEEE-754浮點(diǎn)數(shù)本身的格式,以及“說(shuō)「約」就「約」”(舍入)的規(guī)則,致使精度喪失,計(jì)算淪喪,作為一個(gè)前端,我們就從JS的角度來(lái)扒一扒。
二、端詳一下IEEE-754雙精度浮點(diǎn)的樣貌所謂“知己知彼,百戰(zhàn)不殆”,要從內(nèi)部瓦解敵人,就要先了解敵人,但為什么只選擇雙精度呢,因?yàn)橹懒穗p精度就明白了單精度,而且在JavaScript中,所有的Number都是以64-bit的雙精度浮點(diǎn)數(shù)存儲(chǔ)的,所以我們來(lái)回顧一下到底是怎么存儲(chǔ)的,以及這樣子存儲(chǔ)怎么映射到具體的數(shù)值。
二進(jìn)制在存儲(chǔ)的時(shí)候是以二進(jìn)制的“科學(xué)計(jì)數(shù)法”來(lái)存儲(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”的模式,具體來(lái)說(shuō):
就是1位最高位(最左邊那一位)表示符號(hào)位,0表示正,1表示負(fù)
接下去11位表示指數(shù)部分
最后52位表示尾數(shù)部分,也就是有效域部分
這里幺蛾子就很多了。首先“每個(gè)實(shí)數(shù)都有一個(gè)相反數(shù)”這是中學(xué)教的,于是符號(hào)位改變下就是一個(gè)相反數(shù)了,但是對(duì)于數(shù)字0來(lái)說(shuō),相反數(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呢?要么舍入要么舍棄了,總之是無(wú)效了。所以,尾數(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)后的部分。
問(wèn)題來(lái)了,如果這個(gè)二進(jìn)制小數(shù)太小了,那么會(huì)出現(xiàn)什么情況呢?對(duì)于一個(gè)接近于0的二進(jìn)制小數(shù),一味追求1.xxx的形式,必然導(dǎo)致指數(shù)部分會(huì)向負(fù)無(wú)窮靠攏,而真實(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ù)無(wú)窮是怎么表示的:
這里的(0.f)和(1.f)指的是二進(jìn)制的表示,都要轉(zhuǎn)化為十進(jìn)制再去計(jì)算,這樣你就可以得到最終值。
回顧了IEEE754的64bit浮點(diǎn)數(shù)之后,有以下3點(diǎn)需要牢記的:
指數(shù)和尾數(shù)域是有限的,一個(gè)是11位,一個(gè)是52位
符號(hào)位決定正負(fù),指數(shù)域決定數(shù)量級(jí),尾數(shù)域決定精度
所有數(shù)值的計(jì)算和比較,都是這樣以64個(gè)bit的形式來(lái)進(jìn)行的,拋開腦海中想當(dāng)然的十進(jìn)制
三、精度在哪里發(fā)生丟失當(dāng)你直接計(jì)算0.1+0.2時(shí),你要知道“你大媽已經(jīng)不是你大媽,你大爺也已經(jīng)不是你大爺了,所以他們生的孩子(結(jié)果)出現(xiàn)問(wèn)題就可以理解了”。這里的0.1和0.2是十進(jìn)制下的0.1和0.2,當(dāng)它們轉(zhuǎn)化為二進(jìn)制時(shí),它們是無(wú)限循環(huán)的二進(jìn)制表示。
這引出第一處可能丟失精度的地方,即在十進(jìn)制轉(zhuǎn)二進(jìn)制的過(guò)程中丟失精度。因?yàn)榇蟛糠值氖M(jìn)制小數(shù)是不能被這52位尾數(shù)的二進(jìn)制小數(shù)表示完畢的,我們眼中最簡(jiǎn)單的0.1、0.2在轉(zhuǎn)化為二進(jìn)制小數(shù)時(shí)都是無(wú)限循環(huán)的,還有些可能不是無(wú)限循環(huán)的,但是轉(zhuǎn)化為二進(jìn)制小數(shù)的時(shí)候,小數(shù)部分超過(guò)了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é)果偏差越來(lái)越大哦。
第二處可能丟失精度的地方是浮點(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)制過(guò)程中和相加過(guò)程中都已經(jīng)丟失了精度,那么最后的結(jié)果有問(wèn)題,不能如愿也就不奇怪了,如果你很想探究具體這是怎么計(jì)算的,文末附錄的鏈接能幫助你。
四、疑惑:0.1不能被精確表示,但打印0.1它就是0.1啊是的,照理說(shuō),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ā)生近似,打印出來(lái)的值其實(shí)是近似過(guò)的值,并不是對(duì)浮點(diǎn)數(shù)存儲(chǔ)內(nèi)容的精確反映。
關(guān)于這個(gè)問(wèn)題,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的形式來(lái)進(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è)鬼樣。
來(lái)看第一部分中提出兩個(gè)簡(jiǎn)單的比較問(wèn)題:
//這相等和不等是怎么回事? 0.100000000000000002 == 0.1 //true 0.100000000000000002 == 0.100000000000000010 // true 0.100000000000000002 == 0.100000000000000020 // false
當(dāng)你把0.1、0.100000000000000002、0.10000000000000001和0.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。
再來(lái)看提到的第二個(gè)相等問(wèn)題:
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è)就不行了,首先我們來(lái)轉(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ì)算器來(lái)驗(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。
換句話說(shuō),我們可以說(shuō)對(duì)于1010數(shù)量級(jí),其精確度大約在10-6數(shù)量級(jí),那么對(duì)于109、108、100等等數(shù)量級(jí)的值,精確度又大約在多少呢?
有一張圖很好地說(shuō)明了這個(gè)對(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í)。
也就是說(shuō),在IEEE754的64位浮點(diǎn)數(shù)表示下,如果一個(gè)數(shù)的數(shù)量級(jí)在10X,其精確度在10Y,那么X和Y大致滿足:
X-16=Y
知道這個(gè)之后我們?cè)倩剡^(guò)頭來(lái)看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ù)用來(lái)干嘛呢?
0.1+0.2-0.3
那么怎樣能在計(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)形式精確表示的,換句話說(shuō)這個(gè)范圍內(nèi)的整數(shù)運(yùn)算,結(jié)果都是精確的,而大部分場(chǎng)景下這個(gè)數(shù)的范圍已經(jīng)夠用,所以這種思路可行。
1. JS中數(shù)的“量程”和“精度”之所以說(shuō)一個(gè)范圍,而不是所有的整數(shù),是因?yàn)檎麛?shù)也存在精確度的問(wèn)題,要深刻地理解,”可表示范圍“和”精確度“兩個(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ù)
為什么超過(guò)最大安全數(shù)的整數(shù)都不精確了呢?還是回到IEEE754的那幾個(gè)坑上,尾數(shù)就52個(gè)坑,有效數(shù)再多,就要發(fā)生舍入了。
2. 一段有瑕疵的解決浮點(diǎn)計(jì)算異常問(wèn)題的代碼因此,回到解決JS浮點(diǎn)數(shù)的精確計(jì)算上來(lái),可以把待計(jì)算的小數(shù)轉(zhuǎn)化為整數(shù),在安全整數(shù)范圍內(nèi),再計(jì)算結(jié)果,再轉(zhuǎn)回小數(shù)。
所以有了下面這段代碼(但這是有問(wèn)題的):
//注意要傳入兩個(gè)小數(shù)的字符串表示,不然在小數(shù)轉(zhuǎn)成二進(jìn)制浮點(diǎn)數(shù)的過(guò)程中精度就已經(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; };
思路沒有問(wèn)題,看上去也解決了0.1+0.2的問(wèn)題,用上面的函數(shù)計(jì)算numAdd("0.1","0.2")時(shí),輸出確實(shí)是0.3。但是再多試幾個(gè),比如numAdd("268.34","0.83"),輸出是269.16999999999996,瞬間爆炸,這些代碼一行都不想再看。
其實(shí)仔細(xì)分析一下,這個(gè)問(wèn)題還是很好解決的。問(wèn)題是這么發(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)制過(guò)程中,精度會(huì)損失。
我們可以在上面代碼的return語(yǔ)句之上加上這兩句看看輸出是什么:
console.log(num1 * baseNum); console.log(num2 * baseNum);
你會(huì)發(fā)現(xiàn)針對(duì)numAdd("268.34","0.83")的例子,上面兩行輸出26833.999999999996、83??梢钥吹睫D(zhuǎn)化為整數(shù)的夢(mèng)想并沒有被很好地實(shí)現(xiàn)
要解決這個(gè)問(wèn)題也很容易,就是我們顯式地讓小數(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)呢,那么通過(guò)這個(gè)方式轉(zhuǎn)化出的整數(shù)就超過(guò)了安全整數(shù)的范圍,那么計(jì)算也就不安全了。
不過(guò)還是一句話,看使用場(chǎng)景進(jìn)行選擇,如果局限性不會(huì)出現(xiàn)或者出現(xiàn)了但是無(wú)傷大雅,那就可以應(yīng)用。
另一種思路是將小數(shù)轉(zhuǎn)為字符串,用字符串去模擬,這樣子做可適用的范圍比較廣,但是實(shí)現(xiàn)過(guò)程會(huì)比較繁瑣。
如果你的項(xiàng)目中需要多次面臨這樣的計(jì)算,又不想自己實(shí)現(xiàn),那么也有現(xiàn)成的庫(kù)可以使用,比如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,而不用每次遇到類似問(wèn)題就發(fā)一個(gè)日經(jīng)的問(wèn)題,不會(huì)就知道“IEEE754”這一個(gè)詞的皮毛卻說(shuō)不出一句完整的表達(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)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/82513.html
摘要:一復(fù)習(xí)進(jìn)制轉(zhuǎn)進(jìn)制整數(shù)部分除取余,逆序小數(shù)部分乘取整,正序在線工具二了解雙精度浮點(diǎn)數(shù)規(guī)范通過(guò)進(jìn)制的科學(xué)計(jì)數(shù)法存儲(chǔ)。最終可表示為圖片來(lái)源其中,,都是實(shí)際存儲(chǔ)科學(xué)計(jì)數(shù)法的值。 一、復(fù)習(xí)10進(jìn)制轉(zhuǎn)2進(jìn)制 1)整數(shù)部分:除2取余,逆序2)小數(shù)部分:乘2取整,正序在線工具 二、了解IEEE 754雙精度浮點(diǎn)數(shù)規(guī)范 1) 通過(guò)2進(jìn)制的科學(xué)計(jì)數(shù)法存儲(chǔ)。 和10進(jìn)制的科學(xué)計(jì)數(shù)法類似,二進(jìn)制的科學(xué)技術(shù)法格...
摘要:很多人可能誤認(rèn)為資源跨域時(shí)無(wú)法請(qǐng)求,實(shí)質(zhì)上請(qǐng)求是可以正常發(fā)起的指通常情況下,部分瀏覽器存在部分特例,后端也可能正常進(jìn)行了處理,中文指南調(diào)試技巧指南前端掘金作者緝熙簡(jiǎn)介是推出的一個(gè)天挑戰(zhàn)。 深入 JavaScript,從對(duì)象開始 - 前端 - 掘金入坑前端開發(fā)有一段時(shí)間了,面對(duì)形形色色的JavaScript優(yōu)秀框架,到底該選擇那一款呢?最近在使用Vue.js寫一款markdown編輯器插...
摘要:?jiǎn)栴}實(shí)現(xiàn)字符串類型的數(shù)字相加的一個(gè)方法。總結(jié)好的,最開始提到的問(wèn)題已經(jīng)解決了,準(zhǔn)確的說(shuō),文中的代碼只是實(shí)現(xiàn)了超出范圍的正整數(shù)相加,不支持負(fù)整數(shù)和小數(shù),也許我們可以繼續(xù)去做點(diǎn)什么。 問(wèn)題 實(shí)現(xiàn) 字符串類型的數(shù)字 相加的一個(gè)方法。比如:輸入 11111111111111111 ,22222222222222222,返回 33333333333333333 解決思路 JavaScript 能...
摘要:前言最近,朋友問(wèn)了我這樣一個(gè)問(wèn)題在中的運(yùn)算結(jié)果,為什么是這樣的雖然我告訴他說(shuō),這是由于浮點(diǎn)數(shù)精度問(wèn)題導(dǎo)致的。由于可以用階碼移動(dòng)小數(shù)點(diǎn),因此稱為浮點(diǎn)數(shù)。它的實(shí)現(xiàn)遵循標(biāo)準(zhǔn),使用位精度來(lái)表示浮點(diǎn)數(shù)。 showImg(https://segmentfault.com/img/remote/1460000018981071); 前言 最近,朋友 L 問(wèn)了我這樣一個(gè)問(wèn)題:在 chrome 中的運(yùn)算...
摘要:也就是說(shuō)不僅是會(huì)產(chǎn)生這種問(wèn)題,只要是采用的浮點(diǎn)數(shù)編碼方式來(lái)表示浮點(diǎn)數(shù)時(shí),則會(huì)產(chǎn)生這類問(wèn)題。到這里我們都理解只要采取的浮點(diǎn)數(shù)編碼的語(yǔ)言均會(huì)出現(xiàn)上述問(wèn)題,只是它們的標(biāo)準(zhǔn)類庫(kù)已經(jīng)為我們提供了解決方案而已。 Brief 一天有個(gè)朋友問(wèn)我JS中計(jì)算0.7 * 180怎么會(huì)等于125.99999999998,坑也太多了吧!那時(shí)我猜測(cè)是二進(jìn)制表示數(shù)值時(shí)發(fā)生round-off error所導(dǎo)致,但并不...
閱讀 1505·2021-11-25 09:43
閱讀 2144·2021-07-26 23:38
閱讀 812·2019-08-30 15:53
閱讀 2364·2019-08-30 15:43
閱讀 1247·2019-08-29 18:40
閱讀 2020·2019-08-26 13:28
閱讀 2043·2019-08-23 18:20
閱讀 606·2019-08-23 15:07