「干貨」細(xì)說 Javascript 中的浮點(diǎn)數(shù)精度丟失問題

作者簡(jiǎn)介:
Jaked
8年前端工作經(jīng)驗(yàn),
主要分享:職業(yè)發(fā)展方面腊凶、前端技術(shù)销斟、面試技巧等。
公眾號(hào):超哥前端小棧
掘金:https://juejin.im/user/5a5d4522518825732b19d364/posts

最近抡四,朋友 L 問了我這樣一個(gè)問題:在 chrome 中的運(yùn)算結(jié)果,為什么是這樣的慎璧?

0.55 * 100 // 55.000000000000010.56 * 100 // 56.000000000000010.57 * 100 // 56.999999999999990.58 * 100 // 57.999999999999990.59 * 100 // 590.60 * 100 // 60

雖然我告訴他說床嫌,這是由于浮點(diǎn)數(shù)精度問題導(dǎo)致的。但他還是不太明白胸私,為何有的結(jié)果輸出整數(shù),有的是以 ...001 的小數(shù)結(jié)尾鳖谈,有的卻是以 ...999 的小數(shù)結(jié)尾岁疼,跟預(yù)想中的有差異。

這其實(shí)牽涉到了計(jì)算機(jī)原理的知識(shí)缆娃,真要解釋清楚什么是浮點(diǎn)數(shù)捷绒,恐怕得分好幾個(gè)章節(jié)了。想深入了解的同學(xué)贯要,可以前往 這篇文章 細(xì)讀暖侨。今天我們僅討論浮點(diǎn)數(shù)運(yùn)算結(jié)果的成因,以及如何實(shí)現(xiàn)我們期望的結(jié)果崇渗。

浮點(diǎn)數(shù)與 IEEE 754

在解釋什么是浮點(diǎn)數(shù)之前字逗,讓我們先從較為簡(jiǎn)單的小數(shù)點(diǎn)說起。

小數(shù)點(diǎn)宅广,在數(shù)制中代表一種對(duì)齊方式葫掉。比如要比較 1000 和 200 哪個(gè)比較大,該怎么做呢跟狱?必須把他們右對(duì)齊:

1000 200

發(fā)現(xiàn) 1 比 0(前面補(bǔ)零)大俭厚,所以 1000 比較大。那么如果要比較 1000 和 200.01 呢驶臊?這時(shí)候就不是右對(duì)齊了挪挤,而應(yīng)該是以小數(shù)點(diǎn)對(duì)齊:

1000 200.01

小數(shù)點(diǎn)的位置,在進(jìn)制表示中是至關(guān)重要的关翎。位置差一位整體就要差進(jìn)制倍(十進(jìn)制就是十倍)扛门。在計(jì)算機(jī)中也是這樣,雖然計(jì)算機(jī)使用二進(jìn)制笤休,但在處理非整數(shù)時(shí)尖飞,也需要考慮小數(shù)點(diǎn)的位置問題。無法對(duì)齊小數(shù)點(diǎn),就無法做加減法比較這樣的操作政基。

接下來的一個(gè)重要概念:在計(jì)算機(jī)中的小數(shù)有兩種贞铣,定點(diǎn) 和 浮點(diǎn)。

定點(diǎn)的意思是沮明,小數(shù)點(diǎn)固定在 32 位中的某個(gè)位置辕坝,前面的是整數(shù),后面的是小數(shù)荐健。小數(shù)點(diǎn)具體固定在哪里酱畅,可以自己在程序中指定。定點(diǎn)數(shù)的優(yōu)點(diǎn)是很簡(jiǎn)單江场,大部分運(yùn)算實(shí)現(xiàn)起來和整數(shù)一樣或者略有變化纺酸,但是缺點(diǎn)則是表示范圍太小,精度很差址否,不能充分運(yùn)用存儲(chǔ)單元餐蔬。

浮點(diǎn)數(shù)就是設(shè)計(jì)來克服這個(gè)缺點(diǎn)的,它相當(dāng)于一個(gè)定點(diǎn)數(shù)加上一個(gè)階碼佑附,階碼表示將這個(gè)定點(diǎn)數(shù)的小數(shù)點(diǎn)移動(dòng)若干位樊诺。由于可以用階碼移動(dòng)小數(shù)點(diǎn),因此稱為浮點(diǎn)數(shù)音同。我們?cè)趯懗绦驎r(shí)词爬,用到小數(shù)的地方,用 float 類型表示权均,可以方便快速地對(duì)小數(shù)進(jìn)行運(yùn)算顿膨。

浮點(diǎn)數(shù)在 Javascript 中的存儲(chǔ),與其他語言如 Java 和 Python 不同螺句。所有數(shù)字(包括整數(shù)和小數(shù))都只有一種類型 — Number虽惭。它的實(shí)現(xiàn)遵循 IEEE 754 標(biāo)準(zhǔn),使用64位精度來表示浮點(diǎn)數(shù)蛇尚。它是目前最廣泛使用的格式芽唇,該格式用 64 位二進(jìn)制表示像下面這樣從上圖中可以看出,這 64 位分為三個(gè)部分:

image
  • 符號(hào)位:1 位用于標(biāo)志位取劫。用來表示一個(gè)數(shù)是正數(shù)還是負(fù)數(shù)

  • 指數(shù)位:11 位用于指數(shù)匆笤。這允許指數(shù)最大到 1024

  • 尾數(shù)位:剩下的 52 位代表的是尾數(shù),超出的部分自動(dòng)進(jìn)一舍零

精度丟哪兒去了谱邪?

問:要把小數(shù)裝入計(jì)算機(jī)炮捧,總共分幾步?

答:3 步惦银。

  • 第一步:轉(zhuǎn)換成二進(jìn)制

  • 第二步:用二進(jìn)制科學(xué)計(jì)算法表示

  • 第三步:表示成 IEEE 754 形式

但第一步和第三步都有可能 丟失精度咆课。

十進(jìn)制是給人看的末誓。但在進(jìn)行運(yùn)算之前,必須先轉(zhuǎn)換為計(jì)算機(jī)能處理的二進(jìn)制书蚪。最后喇澡,當(dāng)運(yùn)算完畢后,再將結(jié)果轉(zhuǎn)換回十進(jìn)制殊校,繼續(xù)給人看晴玖。精度就丟失于這兩次轉(zhuǎn)換的過程中。

十進(jìn)制轉(zhuǎn)二進(jìn)制

接下來为流,就具體說說轉(zhuǎn)換的過程呕屎。來看一個(gè)簡(jiǎn)單的例子:

如何將十進(jìn)制的 168.45 轉(zhuǎn)換為二進(jìn)制?

讓我們拆為兩個(gè)部分來解析:

1敬察、整數(shù)部分秀睛。它的轉(zhuǎn)換方法是,除 2 取余法莲祸。即每次將整數(shù)部分除以 2琅催,余數(shù)為該位權(quán)上的數(shù),而商繼續(xù)除以 2虫给,余數(shù)又為上一個(gè)位權(quán)上的數(shù),這個(gè)步驟一直持續(xù)下去侠碧,直到商為 0 為止抹估,最后讀數(shù)時(shí)候,從最后一個(gè)余數(shù)讀起弄兜,一直到最前面的一個(gè)余數(shù)药蜻。

所以整數(shù)部分 168 的轉(zhuǎn)換過程如下:

  • 第一步,將 168 除以 2替饿,商 84语泽,余數(shù)為 0。

  • 第二步视卢,將商 84 除以 2踱卵,商 42 余數(shù)為 0。

  • 第三步据过,將商 42 除以 2惋砂,商 21 余數(shù)為 0。

  • 第四步绳锅,將商 21 除以 2西饵,商 10 余數(shù)為 1。

  • 第五步鳞芙,將商 10 除以 2眷柔,商 5 余數(shù)為 0期虾。

  • 第六步,將商 5 除以 2驯嘱,商 2 余數(shù)為 1镶苞。

  • 第七步,將商 2 除以 2宙拉,商 1 余數(shù)為 0宾尚。

  • 第八步,將商 1 除以 2谢澈,商 0 余數(shù)為 1煌贴。

  • 第九步,讀數(shù)锥忿。因?yàn)樽詈笠晃皇墙?jīng)過多次除以 2 才得到的牛郑,因此它是最高位。讀數(shù)的時(shí)候敬鬓,從最后的余數(shù)向前讀淹朋,即 10101000。

2钉答、小數(shù)部分础芍。它的轉(zhuǎn)換方法是,乘 2 取整法数尿。即將小數(shù)部分乘以 2仑性,然后取整數(shù)部分,剩下的小數(shù)部分繼續(xù)乘以 2右蹦,然后再取整數(shù)部分诊杆,剩下的小數(shù)部分又乘以 2,一直取到小數(shù)部分為 0 為止何陆。如果永遠(yuǎn)不能為零晨汹,就同十進(jìn)制數(shù)的四舍五入一樣,按照要求保留多少位小數(shù)時(shí)贷盲,就根據(jù)后面一位是 0 還是 1 進(jìn)行取舍淘这。如果是 0 就舍掉,如果是 1 則入一位晃洒,換句話說就是慨灭,0 舍 1 入。讀數(shù)的時(shí)候球及,要從前面的整數(shù)開始氧骤,讀到后面的整數(shù)。

所以小數(shù)部分 0.45 (保留到小數(shù)點(diǎn)第四位)的轉(zhuǎn)換過程如下:

  • 第一步吃引,將 0.45 乘以 2筹陵,得 0.9刽锤,則整數(shù)部分為 0,小數(shù)部分為 0.9朦佩。

  • 第二步, 將小數(shù)部分 0.9 乘以 2并思,得 1.8,則整數(shù)部分為 1语稠,小數(shù)部分為 0.8宋彼。

  • 第三步, 將小數(shù)部分 0.8 乘以 2,得 1.6仙畦,則整數(shù)部分為 1输涕,小數(shù)部分為 0.6。

  • 第四步慨畸,將小數(shù)部分 0.6 乘以 2莱坎,得 1.2,則整數(shù)部分為 1寸士,小數(shù)部分為 0.2檐什。

  • 第五步,將小數(shù)部分 0.2 乘以 2弱卡,得 0.4乃正,則整數(shù)部分為 0,小數(shù)部分為 0.4婶博。

  • 第六步烫葬,將小數(shù)部分 0.4 乘以 2,得 0.8凡蜻,則整數(shù)部分為 0,小數(shù)部分為 0.8垢箕。

  • ...

可以看到划栓,從第六步開始,將無限循環(huán)第三条获、四忠荞、五步,一直乘下去帅掘,最后不可能得到小數(shù)部分為 0委煤。因此,這個(gè)時(shí)候只好學(xué)習(xí)十進(jìn)制的方法進(jìn)行四舍五入了修档。但是二進(jìn)制只有 0 和 1 兩個(gè)碧绞,于是就出現(xiàn) 0 舍 1 入的 “口訣” 了,這也是計(jì)算機(jī)在轉(zhuǎn)換中會(huì)產(chǎn)生誤差的根本原因吱窝。但是由于保留位數(shù)很多讥邻,精度很高迫靖,所以可以忽略不計(jì)。

這樣兴使,我們就可以得出十進(jìn)制數(shù) 168.45 轉(zhuǎn)換為二進(jìn)制的結(jié)果系宜,約等于 10101000.0111。

二進(jìn)制轉(zhuǎn)十進(jìn)制

它的轉(zhuǎn)換方法相對(duì)簡(jiǎn)單些发魄,按權(quán)相加法盹牧。就是將二進(jìn)制每位上的數(shù)乘以權(quán),然后相加之和即是十進(jìn)制數(shù)励幼。其中有兩個(gè)注意點(diǎn):要知道二進(jìn)制每位的權(quán)值汰寓,要能求出每位的值。

所以赏淌,將剛才的二進(jìn)制 10101000.0111 轉(zhuǎn)換為十進(jìn)制踩寇,得到的結(jié)果就是 168.4375,再四舍五入一下六水,即 168.45俺孙。

解決方案

正如本文開頭所提到的,在 JavaScript 中進(jìn)行浮點(diǎn)數(shù)的運(yùn)算掷贾,會(huì)有不少奇葩的問題睛榄。在明白了產(chǎn)生問題的根本原因之后,當(dāng)然是想辦法解決啦~

一個(gè)簡(jiǎn)單粗暴的建議是想帅,使用像 mathjs 這樣的庫场靴。它的 API 也挺簡(jiǎn)單的:

// load math.jsconst math = require('mathjs')// functions and constantsmath.round(math.e, 3)             // 2.718math.atan2(3, -3) / math.pi       // 0.75// expressionsmath.eval('12 / (2.3 + 0.7)')     // 4math.eval('12.7 cm to inch')      // 5 inchmath.eval('sin(45 deg) ^ 2')      // 0.5// chainingmath.chain(3)    .add(4)    .multiply(2)    .done()  // 14

但如果在工程中,沒有太多需要進(jìn)行運(yùn)算的場(chǎng)景的話港准,就不建議這么做了旨剥。畢竟引入三方庫也是有成本的,無論是學(xué)習(xí) API浅缸,還是引入庫之后轨帜,帶來打包后的文件體積增積。

那么衩椒,不引入庫該怎么處理浮點(diǎn)數(shù)呢蚌父?

可以從需求出發(fā)。例如毛萌,本文開頭的例子苟弛。可以猜想到阁将,需求可能是要把小數(shù)轉(zhuǎn)為百分比膏秫,通常會(huì)保留兩位小數(shù)。而在一些對(duì)數(shù)字較為敏感的業(yè)務(wù)場(chǎng)景中做盅,可能并不希望對(duì)數(shù)字進(jìn)行四舍五入荔睹,所以 toFixed() 方法就沒法用了狸演。

一種思路是,將小數(shù)點(diǎn)像右多移動(dòng) n 位僻他,取整后再除以 (10 * n)宵距。比如這樣:

0.58 * 10000 / 100 // => 58

ok,搞定~

特別需要注意的是吨拗,在需要四舍五入的場(chǎng)景下满哪,我們會(huì)習(xí)慣用到內(nèi)置方法 toFixed(),但它存在一些問題:

1.35.toFixed(1) // 1.4 正確1.335.toFixed(2) // 1.33  錯(cuò)誤1.3335.toFixed(3) // 1.333 錯(cuò)誤1.33335.toFixed(4) // 1.3334 正確1.333335.toFixed(5)  // 1.33333 錯(cuò)誤1.3333335.toFixed(6) // 1.333333 錯(cuò)誤

另外劝篷,它的返回結(jié)果類型是 String哨鸭。不能直接拿來做運(yùn)算,因?yàn)橛?jì)算機(jī)會(huì)認(rèn)為是 字符串拼接娇妓。

總結(jié)

計(jì)算機(jī)在做運(yùn)算的時(shí)候像鸡,會(huì)分三個(gè)步驟。其中哈恰,將十進(jìn)制轉(zhuǎn)為二進(jìn)制只估,再將二進(jìn)制轉(zhuǎn)為十進(jìn)制的時(shí)候,都會(huì)產(chǎn)生精度丟失着绷。

使用庫蛔钙,是最簡(jiǎn)單粗暴的解決方案。但如果使用不頻繁荠医,還是要根據(jù)需求吁脱,手動(dòng)解決。在使用內(nèi)置方法 toFixed() 的時(shí)候彬向,要特別注意它的返回類型兼贡,不要直接拿來做運(yùn)算。

作者簡(jiǎn)介:
Jaked
8年前端工作經(jīng)驗(yàn)娃胆,
主要分享:職業(yè)發(fā)展方面紧显、前端技術(shù)、面試技巧等缕棵。
公眾號(hào):超哥前端小棧
掘金:https://juejin.im/user/5a5d4522518825732b19d364/posts

本文已經(jīng)獲得Jaked老師授權(quán)轉(zhuǎn)發(fā),其他人若有興趣轉(zhuǎn)載涉兽,請(qǐng)直接聯(lián)系作者授權(quán)招驴。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市枷畏,隨后出現(xiàn)的幾起案子别厘,更是在濱河造成了極大的恐慌,老刑警劉巖拥诡,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件触趴,死亡現(xiàn)場(chǎng)離奇詭異氮发,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)冗懦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門爽冕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人披蕉,你說我怎么就攤上這事颈畸。” “怎么了没讲?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵眯娱,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我爬凑,道長(zhǎng)徙缴,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任嘁信,我火速辦了婚禮于样,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘吱抚。我一直安慰自己百宇,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布秘豹。 她就那樣靜靜地躺著携御,像睡著了一般。 火紅的嫁衣襯著肌膚如雪既绕。 梳的紋絲不亂的頭發(fā)上啄刹,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音凄贩,去河邊找鬼誓军。 笑死,一個(gè)胖子當(dāng)著我的面吹牛疲扎,可吹牛的內(nèi)容都是我干的昵时。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼椒丧,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼壹甥!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起壶熏,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤句柠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體溯职,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡精盅,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了谜酒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片叹俏。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖甚带,靈堂內(nèi)的尸體忽然破棺而出她肯,到底是詐尸還是另有隱情,我是刑警寧澤鹰贵,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布晴氨,位于F島的核電站,受9級(jí)特大地震影響碉输,放射性物質(zhì)發(fā)生泄漏籽前。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一敷钾、第九天 我趴在偏房一處隱蔽的房頂上張望枝哄。 院中可真熱鬧,春花似錦阻荒、人聲如沸挠锥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蓖租。三九已至,卻和暖如春羊壹,著一層夾襖步出監(jiān)牢的瞬間蓖宦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工油猫, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留稠茂,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓情妖,卻偏偏與公主長(zhǎng)得像睬关,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子毡证,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

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