在JavaScript開發(fā)者的開發(fā)生涯中的某些點(diǎn),總會(huì)遇到奇怪的BUG——看似基礎(chǔ)的數(shù)學(xué)問題,但卻又覺得有些不對(duì)勁??傆幸惶?,你會(huì)被告知JavaScript中的數(shù)字實(shí)際上是浮點(diǎn)數(shù)。試圖了解浮點(diǎn)數(shù)和為什么他們?nèi)绱似婀?,迎接你的將是一片又臭又長的文章。本文的目的是給JavaScript開發(fā)者簡單講解浮點(diǎn)數(shù)。
本文假設(shè)讀者熟悉的用二進(jìn)制表示的十進(jìn)制數(shù)字(即1被寫成1b
,2是10b
,3是11b
,4是100b
……等)。為了使文章表達(dá)的更清楚,在本章中,“十進(jìn)制”主要是指計(jì)算機(jī)內(nèi)部的十進(jìn)制數(shù)字表示法(例如:2.718)?!岸M(jìn)制”在本文中指計(jì)算機(jī)內(nèi)部的表示。書面陳述將分別被稱為“以十為底″和“以二為底″。
什么是浮點(diǎn)數(shù),我們開始認(rèn)為我們見過各種數(shù)字,我可可以說1
是一個(gè)整數(shù),因?yàn)樗鼪]有分?jǐn)?shù)部分。
?被稱為分?jǐn)?shù)。這意味著,將一平均分開為二,分?jǐn)?shù)是浮點(diǎn)運(yùn)算中一個(gè)非常重要的概念。
0.5
通常被稱為一個(gè)十進(jìn)制數(shù)。然而,有一個(gè)很重要的區(qū)別必須闡明——0.5
實(shí)際上是分?jǐn)?shù)?的十進(jìn)制(以十為底)表示。本文中,我們將這種表示方法稱為點(diǎn)表示法。我們把0.5
稱為有限表示(有限小數(shù))因?yàn)槠浞謹(jǐn)?shù)表示的數(shù)字是有限的——5
后面沒有其他數(shù)字。表示?的0.3333
…是無限表示的例子。這個(gè)想法在我們的討論非常重要。
還存在另一種表示全部整數(shù),分?jǐn)?shù)或小數(shù)的方法。你可能已經(jīng)見過。它看起來像這樣:6.022×1023(注:這是阿伏伽德羅數(shù),這是摩爾的化學(xué)溶液中的分子的數(shù)目)。它通常被稱為標(biāo)準(zhǔn)形式,或科學(xué)記數(shù)法。形式可以被抽象為像下面這樣:
D1.D2D3D4...Dp x BE
這種通用形式被稱作浮點(diǎn)數(shù)。
由p
和D
組成的序列——D1.D2D3D4...Dp
——被稱為有效數(shù)字或尾數(shù)。p
是有效數(shù)字的權(quán)重,通常稱為精度。有效數(shù)后的x
是符號(hào)的一部分(本文中的乘法符號(hào),將用*表示)。其后是基數(shù),基數(shù)后是指數(shù)。該指數(shù)可以是正或負(fù)。
浮點(diǎn)數(shù)的好處是它可以用來表示任何數(shù)值。例如,整數(shù)1可以表示為1.0×10^0
。光的速度可以表示為2.99792458×108 m/s
。1/2可以被表示為二進(jìn)制形式0.1×2^0
。
在上面的例子中,我們?nèi)匀槐A粜?shù)點(diǎn)(小數(shù)點(diǎn)在數(shù)字里面)。當(dāng)用二進(jìn)制表示數(shù)值的時(shí)候,這帶來了一些問題。任意給定一個(gè)浮點(diǎn)數(shù),比如π
(PI),我們可以將其表示為一個(gè)浮點(diǎn)數(shù):3.14159 x 100
。用二進(jìn)制表示,它看起來像這樣:11.00100100 001111……
假設(shè)在十六位機(jī)里表示數(shù)字,這意味著數(shù)字被放在機(jī)器里會(huì)是這樣的:11001001000011111
?,F(xiàn)在的問題是:小數(shù)點(diǎn)應(yīng)該放在哪里?這甚至不涉及指數(shù)(我們默認(rèn)基數(shù)為2)。
如果數(shù)字變?yōu)?code>5.14159?整數(shù)部分將變?yōu)?code>101而不是11
,增加了一位。當(dāng)然,我們可以指定字段的前N位屬于整數(shù)部分(即小數(shù)點(diǎn)的左邊),其余屬于小數(shù)部分,但那是另一篇關(guān)于定點(diǎn)數(shù)的話題。
一旦我們移除小數(shù)點(diǎn)后,我們只有兩件東西需要記錄:指數(shù)和尾數(shù)。我們可以通過應(yīng)用變換公式將小數(shù)點(diǎn)移除,使廣義浮點(diǎn)數(shù)看起來像這樣:
D1D2D3D4...Dp / (Bp-1) x BE
這就是我們得到的大多數(shù)二進(jìn)制浮點(diǎn)數(shù)。注意,現(xiàn)在有效數(shù)是一個(gè)整數(shù)。這使得它更易于存儲(chǔ)一個(gè)浮點(diǎn)數(shù)在機(jī)器上。事實(shí)上,應(yīng)用最廣泛的二進(jìn)制浮點(diǎn)數(shù)表示方法是IEEE 754標(biāo)準(zhǔn)。
JavaScript中的浮點(diǎn)數(shù)采用IEEE-754格式的規(guī)定。更具體的說是一個(gè)雙精度格式,這意味著每個(gè)浮點(diǎn)數(shù)占64位。雖然它不是二進(jìn)制表示浮點(diǎn)數(shù)的唯一途徑,但它是目前最廣泛使用的格式。該格式用64位二進(jìn)制表示像下面這樣:
你可能注意到機(jī)器表示的方法和約定俗成的書面表示一點(diǎn)不同。在64位中,1位用于標(biāo)志位——用來表示一個(gè)數(shù)是正數(shù)還是負(fù)數(shù)。11位用于指數(shù)–這允許指數(shù)最大到1024
。剩下的52位代表的尾數(shù)。如果你曾經(jīng)好奇為什么JavaScript中的某些東西如+0
和 -0
,標(biāo)志位說明一切——JavaScript中的所有數(shù)字都有符號(hào)位。Infinity
和NaN
也被編碼進(jìn)浮點(diǎn)數(shù)——2047
作為一個(gè)特殊的指數(shù)。如果尾數(shù)是0
,它是一個(gè)正無窮或負(fù)無限。如果不是,那么它是NaN
。
有了上面對(duì)浮點(diǎn)數(shù)進(jìn)行介紹,現(xiàn)在我們進(jìn)入了一個(gè)更棘手的問題–舍入誤差。它是所有開發(fā)者使用浮點(diǎn)數(shù)開發(fā)的禍根,JavaScript開發(fā)者尤其如此,因?yàn)镴avaScript開發(fā)者唯一可用的編號(hào)格式是浮點(diǎn)數(shù)。
上面提到的分?jǐn)?shù)?不能在以10為底中有限表示。這實(shí)際上在任何數(shù)制中都存在。例如,在在以二為底的數(shù)字中,1 / 10不能有限表示。被表示為0.00110011001100110011……
注意0011
是無限重復(fù)的。這是因?yàn)檫@個(gè)特別的怪癖,舍入誤差造成的。
先看一個(gè)舍入誤差的例子??紤]一個(gè)最著名的無理數(shù),PI:3.141592653589793……
大多數(shù)人記得前五位(3.1415
)非常棒——我們將使用這個(gè)例子說明舍入誤差,因此可以計(jì)算舍入誤差:
(R - A) / Bp-1 ……其中`R`代表圓形的半徑,`A`代表一個(gè)實(shí)數(shù)。`Bp`代表以`p`為底的精度。所以謹(jǐn)記PI的舍入誤差:`0.00009265……`。
雖然這看起似乎不是很嚴(yán)重,讓我們試著用以二為底的數(shù)來檢驗(yàn)這個(gè)想法。考慮分?jǐn)?shù)1 / 10。在十進(jìn)制,它被寫作0.1
。在二進(jìn)制中,它是:0.00011001100110011……
假設(shè)我們僅保留5位尾數(shù),可以寫為0.0001
。但0.0001
在二進(jìn)制表示法中實(shí)際是1 / 16(或0.0625
)的表示!這意味著有舍入誤差為0.0375
,這是相當(dāng)大的。想象一下基本的加法運(yùn)算,如0.1 + 0.2
,答案返回0.2625
!
幸運(yùn)的是,浮點(diǎn)規(guī)范指定ECMAScript最多使用52個(gè)尾數(shù),所以舍入誤差變得很小——規(guī)范的具體細(xì)節(jié)規(guī)避了大部分的舍入誤差。因?yàn)閷?duì)浮點(diǎn)數(shù)進(jìn)行算術(shù)運(yùn)算的過程中誤差會(huì)被放大,IEEE 754規(guī)范還包括用于數(shù)學(xué)運(yùn)算的具體算法。
然而,應(yīng)該指出的是,盡管如此,算術(shù)運(yùn)算的關(guān)聯(lián)屬性(比如加法,減法,乘法和減法)不能得到保證在處理浮點(diǎn)數(shù)時(shí),即使精度再高。我的意思是,((x + y)+ A + B)
不一定等于((x + y)+(A + B))
。
這是JavaScript開發(fā)人員的禍根。例如,在JavaScript中,0.1 + 0.2 = = = 0.3
將返回假。我希望你現(xiàn)在明白這是為什么。更糟的是,事實(shí)上,舍入誤差會(huì)在連續(xù)的數(shù)學(xué)運(yùn)算中增加(積累)。
設(shè)計(jì)處理JavaScript數(shù)字的問題,已經(jīng)存在很多的建議,好壞參半。大多數(shù)這些建議都是在算數(shù)運(yùn)算之前或之后完成取舍。
到目前位置我見過的寥寥無幾的建議就是把運(yùn)算數(shù)全部存儲(chǔ)為整數(shù)(無類型),然后格式化顯示。通過一個(gè)例子可以看出,在賬戶中大量儲(chǔ)存的美分而不是美元(不知道舉的例子是什么賬戶)。這里有一個(gè)值得注意的問題——不是世界上所有的貨幣都是十進(jìn)制的(毛里求斯幣:毛里求斯盧比是毛里求斯共和國的流通貨幣。幣值有25、50、100、200、500、1000和2000。輔幣單位為分)。同時(shí),吐槽了日元和人名幣……。最終,你會(huì)重新創(chuàng)建浮點(diǎn)——有可能。
我見過處理浮點(diǎn)數(shù)最好的建議是使用庫,像sinfuljs或mathjs。我個(gè)人比較喜歡mathjs(但實(shí)際上,任何和數(shù)學(xué)相關(guān)的我甚至不會(huì)使用JavaScript去做)。當(dāng)需要任意精度數(shù)學(xué)計(jì)算的時(shí)候,BigDecimal也是非常有用的。
另一個(gè)被多次重復(fù)的建議是使用內(nèi)置的toPrecision()
和toFixed()
方法。使用他們時(shí)最容易犯得邏輯錯(cuò)誤是忘記這些方法的返回值字符串。所以如果你像下面這樣會(huì)得不到想要的結(jié)果:
function foo(x, y) {
return x.toPrecision() + y.toPrecision()
}
> foo(0.1, 0.2)
"0.10.2"
設(shè)計(jì)內(nèi)置方法toPrecision()
和toFixed()
的目的僅是用于顯示。謹(jǐn)慎使用!
JavaScript中的數(shù)字是真正的浮點(diǎn)數(shù)。由于二進(jìn)制表示的固有缺陷,以及有限的機(jī)器空間,我們不得不面對(duì)一個(gè)充滿舍入誤差的規(guī)范。本文解釋了為什么這些舍入誤差是什么和為什么。記住使用一個(gè)很棒的庫而不是自己去做一切。
原文:http://flippinawesome.org/2014/02/17/what-every-javascript-developer-should-know-about-floating-points/
更多建議: