每一個JavaScript開發者應該瞭解的浮點知識

顏海鏡發表於2014-03-26

在JavaScript開發者的開發生涯中的某些點,總會遇到奇怪的BUG——看似基礎的數學問題,但卻又覺得有些不對勁。總有一天,你會被告知JavaScript中的數字實際上是浮點數。試圖瞭解浮點數和為什麼他們如此奇怪,迎接你的將是一片又臭又長的文章。本文的目的是給JavaScript開發者簡單講解浮點數。

本文假設讀者熟悉的用二進位制表示的十進位制數字(即1被寫成1b,2是10b,3是11b,4是100b……等)。為了使文章表達得更清楚,在本章中,“十進位制”主要是指計算機內部的十進位制數字表示法(例如:2.718)。“二進位制”在本文中指計算機內部的表示。書面陳述將分別被稱為“以十為底″和“以二為底″。

浮點數

什麼是浮點數,我們開始認為我們見過各種數字,我可可以說1是一個整數,因為它沒有分數部分。

½被稱為分數。這意味著,將一平均分開為二,分數是浮點運算中一個非常重要的概念。

0.5通常被稱為一個十進位制數。然而,有一個很重要的區別必須闡明——0.5實際上是分數½的十進位制(以十為底)表示。本文中,我們將這種表示方法稱為點表示法。我們把0.5稱為有限表示(有限小數)因為其分數表示的數字是有限的——5後面沒有其他數字。表示⅓的0.3333…是無限表示的例子。這個想法在我們的討論非常重要。

還存在另一種表示全部整數,分數或小數的方法。你可能已經見過。它看起來像這樣:6.022×1023(注:這是阿伏伽德羅數,這是摩爾的化學溶液中的分子的數目)。它通常被稱為標準形式,或科學記數法。形式可以被抽象為像下面這樣:

D1.D2D3D4...Dp x BE

這種通用形式被稱作浮點數

pD組成的序列——D1.D2D3D4...Dp——被稱為有效數字或尾數。p是有效數字的權重,通常稱為精度。有效數後的x是符號的一部分(本文中的乘法符號,將用*表示)。其後是基數,基數後是指數。該指數可以是正或負。

浮點數的好處是它可以用來表示任何數值。例如,整數1可以表示為1.0×10^0。光的速度可以表示為2.99792458×108 m/s。1/2可以被表示為二進位制形式0.1×2^0

移除小數點

在上面的例子中,我們仍然保留小數點(小數點在數字裡面)。當用二進位制表示數值的時候,這帶來了一些問題。任意給定一個浮點數,比如π(PI),我們可以將其表示為一個浮點數:3.14159 x 100。用二進位制表示,它看起來像這樣:11.00100100 001111……假設在十六位機裡表示數字,這意味著數字被放在機器裡會是這樣的:11001001000011111。現在的問題是:小數點應該放在哪裡?這甚至不涉及指數(我們預設基數為2)。

如果數字變為5.14159?整數部分將變為101而不是11,增加了一位。當然,我們可以指定欄位的前N位屬於整數部分(即小數點的左邊),其餘屬於小數部分,但那是另一篇關於定點數的話題。

一旦我們移除小數點後,我們只有兩件東西需要記錄:指數和尾數。我們可以通過應用變換公式將小數點移除,使廣義浮點數看起來像這樣:

D1D2D3D4...Dp / (Bp-1) x BE

這就是我們得到的大多數二進位制浮點數。注意,現在有效數是一個整數。這使得它更易於儲存一個浮點數在機器上。事實上,應用最廣泛的二進位制浮點數表示方法是IEEE 754標準。

IEEE 754

JavaScript中的浮點數採用IEEE-754格式的規定。更具體的說是一個雙精度格式,這意味著每個浮點數佔64位。雖然它不是二進位制表示浮點數的唯一途徑,但它是目前最廣泛使用的格式。該格式用64位二進位制表示像下面這樣:

你可能注意到機器表示的方法和約定俗成的書面表示一點不同。在64位中,1位用於標誌位——用來表示一個數是正數還是負數。11位用於指數–這允許指數最大到1024。剩下的52位代表的尾數。如果你曾經好奇為什麼JavaScript中的某些東西如+0-0,標誌位說明一切——JavaScript中的所有數字都有符號位。InfinityNaN也被編碼進浮點數——2047作為一個特殊的指數。如果尾數是0,它是一個正無窮或負無限。如果不是,那麼它是NaN

舍入誤差

有了上面對浮點數進行介紹,現在我們進入了一個更棘手的問題–舍入誤差。它是所有開發者使用浮點數開發的禍根,JavaScript開發者尤其如此,因為JavaScript開發者唯一可用的編號格式是浮點數。

上面提到的分數⅓不能在以10為底中有限表示。這實際上在任何數制中都存在。例如,在在以二為底的數字中,1 / 10不能有限表示。被表示為0.00110011001100110011……注意0011是無限重複的。這是因為這個特別的怪癖,舍入誤差造成的。

先看一個舍入誤差的例子。考慮一個最著名的無理數,PI:3.141592653589793……大多數人記得前五位(3.1415)非常棒——我們將使用這個例子說明舍入誤差,因此可以計算舍入誤差:

(R - A) / Bp-1

……其中R代表圓形的半徑,A代表一個實數。Bp代表以p為底的精度。所以謹記PI的舍入誤差:0.00009265……

雖然這看起似乎不是很嚴重,讓我們試著用以二為底的數來檢驗這個想法。考慮分數1 / 10。在十進位制,它被寫作0.1。在二進位制中,它是:0.0011001100110011……假設我們僅保留5位尾數,可以寫為0.0001。但0.0001在二進位制表示法中實際是1 / 16(或0.0625)的表示!這意味著有舍入誤差為0.0375,這是相當大的。想象一下基本的加法運算,如0.1 + 0.2,答案返回0.2625

幸運的是,浮點規範指定ECMAScript最多使用52個尾數,所以舍入誤差變得很小——規範的具體細節規避了大部分的舍入誤差。因為對浮點數進行算術運算的過程中誤差會被放大,IEEE 754規範還包括用於數學運算的具體演算法。

然而,應該指出的是,儘管如此,算術運算的關聯屬性(比如加法,減法,乘法和減法)不能得到保證在處理浮點數時,即使精度再高。我的意思是,((x + y)+ A + B)不一定等於((x + y)+(A + B))

這是JavaScript開發人員的禍根。例如,在JavaScript中,0.1 + 0.2 = = = 0.3將返回假。我希望你現在明白這是為什麼。更糟的是,事實上,舍入誤差會在連續的數學運算中增加(積累)。

在JavaScript處理浮點數

設計處理JavaScript數字的問題,已經存在很多的建議,好壞參半。大多數這些建議都是在算數運算之前或之後完成取捨。

到目前位置我見過的寥寥無幾的建議就是把運算數全部儲存為整數(無型別),然後格式化顯示。通過一個例子可以看出,在賬戶中大量儲存的美分而不是美元(不知道舉的例子是什麼賬戶)。這裡有一個值得注意的問題——不是世界上所有的貨幣都是十進位制的(模里西斯幣:模里西斯盧比是模里西斯共和國的流通貨幣。幣值有25、50、100、200、500、1000和2000。輔幣單位為分)。同時,吐槽了日元和人民幣……。最終,你會重新建立浮點——有可能。

我見過處理浮點數最好的建議是使用庫,像sinfuljsmathjs。我個人比較喜歡mathjs(但實際上,任何和數學相關的我甚至不會使用JavaScript去做)。當需要任意精度數學計算的時候,BigDecimal也是非常有用的。

另一個被多次重複的建議是使用內建的toPrecision()toFixed()方法。使用他們時最容易犯得邏輯錯誤是忘記這些方法的返回值字串。所以如果你像下面這樣會得不到想要的結果:

function foo(x, y) {
    return x.toPrecision() + y.toPrecision()
}

> foo(0.1, 0.2)
"0.10.2"

設計內建方法toPrecision()toFixed()的目的僅是用於顯示。謹慎使用!

結論

JavaScript中的數字是真正的浮點數。由於二進位制表示的固有缺陷,以及有限的機器空間,我們不得不面對一個充滿舍入誤差的規範。本文解釋了為什麼這些舍入誤差是什麼和為什麼。記住使用一個很棒的庫而不是自己去做一切。

原文:http://flippinawesome.org/2014/02/17/what-every-javascript-developer-should-know-about-floating-points/

Q群推薦

  • CSS家園188275051,CSS開發者的天堂,歡迎有興趣的同學加入
  • JavaScript家園159973528,JavaScript開發者的天堂,歡迎有興趣的同學加入
  • GitHub家園225932282,GitHub愛好者的天堂,歡迎有興趣的同學加入
  • 碼農之家203145707,碼農的天堂,歡迎有興趣的同學加入
  • 畢業設計無憂群—IT 307682157,代做畢業設計,歡迎加入

相關文章