在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 這種通用形式被稱作浮點數。 由p和D組成的序列——D1.D2D3D4...Dp——被稱為有效數字或尾數。p是有效數位權重,通常稱為精度。有效數後的x是符號的一部分(本文中的乘法符號,將用*表示)。其後是基數,基數後是指數。該指數可以是正或負。 浮點數的好處是它可以用來表示任何數值。例如,整數1可以表示為1.0×100。光的速度可以表示為2.99792458×108 m/s。1/2可以被表示為二進位形式0.1×20。 移除小數點 在上面的例子中,我們仍然保留小數點(小數點在數字裡面)。當用二進位表示數值的時候,這帶來了一些問題。任意給定一個浮點數,比如π(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位二進位表示像下面這樣: SIGNEXPONENTMANTISSA 你可能注意到機器表示的方法和約定俗成的書面表示一點不同。在64位中,1位用於標誌位——用來表示一個數是正數還是負數。11位用於指數–這允許指數最大到1024。剩下的52位代表的尾數。如果你曾經好奇為什麼JavaScript中的某些東西如+0 和 -0,標誌位說明一切——JavaScript中的所有數字都有符號位。Infinity和NaN也被編碼進浮點數——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。輔幣單位為分)。同時,吐槽了日元和人名幣……。最終,你會重新建立浮點——有可能。 我見過處理浮點數最好的建議是使用庫,像sinfuljs或mathjs。我個人比較喜歡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中的數字是真正的浮點數。由於二進位表示的固有缺陷,以及有限的機器空間,我們不得不面對一個充滿舍入誤差的規範。本文解釋了為什麼這些舍入誤差是什麼和為什麼。記住使用一個很棒的庫而不是自己去做一切。