Js 與浮點數


同步發表在我的博客:jmingzi

當你學習一個知識點沒有方向時,可以嘗試以解決問題的角度來理解它。

例如這個知識點我們可以從以下問題開始:

  • 你看的到 1 真的是整數 1 嗎?
  • 為什么0.1 + 0.2 得到的是 0.30000000000000004 而不是 0.30000000000000004999 ?
  • 為什么最大安全數是 2^53 - 1 ?
  • 如何避免精度問題?
  • 構造函數 Number 的一些靜態屬性

問題一

我們需要知道 js 中沒有真正的整數,我們看到的數值都是 v8 引擎省略精度后的結果。在 ecma-262 規范中並沒有說明該如何省略精度,所以如果換個解析引擎,可能又是另外一種結果了。

1..toPrecision(4) === '1.000'

我們可以用 toPrecision 來獲取數值精度的字符串表示,所以對 js 中的數你得有 “橫看成嶺側成峰” 的感覺。

問題二

由問題一我們知道精度是引擎處理的結果,那么這個問題也是同樣的道理,0.1 + 0.2 如果放大精度來看,得到的當然是

(0.1 + 0.2)..toPrecision(21) === '0.300000000000000044409' 

猜測:小數部分的精度默認是非 0 的 17 位,如果末尾是 0 則繼續省略。接下來,我們從驗證問題三開始從原理入手。

問題三

javascript 的浮點數采用的是 IEEE 754 雙精度 64 位表示,IEEE 754 規定了四種浮點數的表示方式,雙精度 64 位是其中的一種。

64 位二進制組成:

 

  • 第 1 位符號位,用 S 表示,0 代表正數,1 代表負數
  • 第 2 - 12 位指數位,用 E 表示,即 2^11 = 2048,曲值范圍是 0 ~ 2047,由於是雙精度,所以指數部分有正數也有負數,取中值 1023 分隔,即指數值為 E - 1023
  • 第 13 - 64 位尾數位,用 M 表示

例如數字 1 用 二進制表示也是 “1”,如果我們填充 64 位二進制,表示應該是怎樣的呢?

  • 符號 S = 0
  • 指數 E - 1023 = 0,E = 1023
  • 尾數 M = 52

那么實際我們看到的二進制存儲應該是這樣的:

0 01111111111 0000...0000

用在線轉換工具轉換后如下圖,可以驗證結果的正確性:

 

 

http://www.binaryconvert.com/result_double.html

我們再用 0.1 + 0.2 驗證下:

0.1.toString(2) 
// 0.00011001100...11010

0.1 的二進制 64 位轉成科學記數法表示:1.10011001100...*10^-4,也就是說

  • 符號 S = 0
  • 指數 E = 1023 - 4 = 1019
  • 尾數 M = 10011001100...11010 我們可以繼續用在線轉換的網址驗證下結果:

 

 

我們可以發現,其實 0.1 的尾數是 1100 不斷循環的,但是我們看到的最后 4 位是 1010 這是由於尾數只能保存 52 位,多余的部分會被舍棄,舍棄規則是 IEEE 754 規范所定義的:

  • 舍入到最接近:舍入到最接近,在一樣接近的情況下偶數優先(Ties To Even,這是默認的舍入方式),會將結果舍入為最接近且可以表示的值,但是當存在兩個數一樣接近的時候,則取其中的偶數(在二進制中式以0結尾的)
  • 朝+∞方向舍入
  • 朝-∞方向舍入
  • 朝 0 方向舍入

另外,規范還約定,由於二進制的科學記數法永遠是 1.幾 開頭,所以將 1 省略,這樣尾數就有 53 位來表示。所以 0.1 的二進制尾數部分:11001100...11001 這樣 53 位,最后一位 1 向偶數舍入即進 1,得到 1010,這樣得到的數其實是比真實的 0.1 要大的。

同理,我們查看 0.2 的表示:0.00110011001100...11010,可以得到尾數部分其實是一樣的,僅僅是指數少了 1 位。

 

我們再來看看問題三,為什么最大安全整數是 2^53 - 1?我們可以反過來驗證為什么 2^53 已經不再“安全”了。

 

由問題一我們知道正指數最大為 2047 - 1023 = 1024,為什么不是 2^1024 呢?由上我們知道尾數其實是可以有 53 位表示的(省略的 1 位),即

// Math.pow(2, 53).toString(2)
2^53 用二進制表示:1000...000,1 個 1,53 個 0 
// 此時的尾數最多為 53 位,第 54 位 0 會被舍去

2^53-1 用二進制表示:1111...111,53 個 1
2^53+1 用二進制表示:1000...0001,1 個 1,52 個 0,1 個 1 

其中,2^53 + 1 由於尾數最多為 53 位,所以必須舍掉第 54 位 1,根據舍入規則,向偶數舍入,所以舍掉第 54 位 1,不進 1。於是得到

2^53 === 2^53+1

 

也就是說從 2^53 開始就不能唯一表示一個數了,所以才說 2^53-1 是最大的安全數。

問題四

前端避免精度的場景就是展示某個價格,例如下面的公式:

展示價格 = 商品價格 * 數量 + 總價 * 服務費比例 - 優惠券價格

我們常用的方法有 Number.toFixed()、Number.parseFloat()、Math.round(),它們的區別是什么,弄清楚后才能知道如何使用它們。

Number.toFixed(digits) 返回指定位數的字符串表示,會進行四舍五入。例如

1.005.toFixed(2) // 1.00

很顯然不符合我們需求,為什么會這樣呢?因為 1.005 這個數在 64 位二進制存儲時是不能完全表示這個數的,我們放大精度看看

1.005.toPrecision(17)
// 1.0049999999999999

所以四舍五入的時候就將 499...99 舍掉了。

還記得問題三么?0.300000000000000044409 ,猜測的是是小數位 17 位,超過 17 位的部分會被四舍五入。因為做精度運算時都會做四舍五入:

1.005.toPrecision(16)
// 1.005000000000000

1.005.toPrecision(17)
// 1.0049999999999999

1.005.toPrecision(18)
// 1.00499999999999989

所以使用 toFixed 去做展示運算是不可靠的。

Number.parseFloat === parseFloat ,將字符串轉換為浮點數表示,很顯然轉換后顯示出來也會有精度問題,因為精確到哪一位呢?

我上面猜測說是:“小數部分的精度默認是非 0 的 17 位”,這是不准確的,例如:

1.005 * 100
// 100.49999999999999
100.49999999999999.toPrecision(20)
// 100.49999999999998579
'49999999999999'.length
// 14 並不是 17

所以 parseFloat 后的結果和我們直接寫出來看到的數字是一樣的,並不能夠使用它直接參與計算。

Math.round() 返回一個數字四舍五入后最接近的整數,所以我們一般將浮點數放大為精度位的整數后再使用 Math.round() 得到四舍五入后的整數,再縮小精度位。

例如對於價格類,精度為 2,我們可以先乘 100 做運算后再除 100,此時得到的很可能是個精度位很長的數,我們只需要在展示時乘 100,Math.round 后再除 100 即可。

所以結論就是對於浮點數的計算,先放大,再做計算,計算完成后需要展示精度,可以使用 Math.round 四舍五入后,再縮小即可。

對於不需要做計算的浮點數直接展示,我們可以先 toPrecision 放大精度,再使用 toFixed() 到指定的精度。前提是放大的精度足夠大,最好是 17 。

function round(num, precision) {
  const base = Math.pow(10, precision)
  return Math.round((num.toPrecision(17) * base).toFixed(1)) / base
}
round(1.005, 2)
// 1.01

問題五

Number 構造函數擁有的靜態屬性如下(負方向已忽略):

// 最大能表示的值,無限接近於 2^1024 
Number.MAX_VALUE
// 最大安全數 2^53 - 1
Number.MAX_SAFE_INTEGER
// 正無窮大 2^1024
Number.POSITIVE_INFINITY === Infinity
// 非數值
Number.NaN 等同於 NaN

關於 Infinity 和 NaN 需要注意的是

  • Infinity === Infinity
  • NaN !== NaN
  • Infinity / Infinity = NaN
  • 0 * Infinity = NaN
  • 任何數 和 NaN 運算結果都是 NaN
  • 任何數 / Infinity = 0
  • Infinity * Infinity = Infinity

關注我的公眾號,獲取更多干貨~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM