js數值精度

二口達達發表於2019-03-02

19年目標:消滅英語!我新開了一個公眾號記錄一個程式設計師學英語的歷程

有提升英語訴求的小夥伴可以關注公眾號:csenglish 程式設計師學英語,每天花10分鐘交作業,跟我一起學英語吧


近期在專案中有出現大數值的訂單號9148368244236619在呼叫介面時自動變成9148368244236620的情況,導致請求失誤。本文特意總結了出現這種情況的原因,以及js精度相關的情況。

jquery[.data()]方法

在本次案例中,訂單號是後端同步渲染到頁面上的,

<div class="j_OrderNumber" data-ordernumber="9148368244236619"></div>
複製程式碼

呈現在頁面上的訂單號數值沒有問題9148368244236619,前端此時想獲取到這個訂單號:

var orderNumber = $(`.j_OrderNumber).data(`ordernumber`); // 9148368244236620

typeof(orderNumber) === `number` // true
複製程式碼

在這個取值賦值的過程中,超過安全值的大數值發生了變化,從9148368244236619變成了91483682442366120,這裡的問題主要出現在jquerydata方法。

超過安全值Math.pow(2, 53)-1 -- 9007199254740991 的number型別在賦值的過程中會發生精度丟失,而我們在使用jquery的data方法取得的自定義html屬性data-ordernumber值會強制轉換成number型別,導致精度丟失。

而使用js原生的方法取dom節點的屬性時,獲取到的值都是string型別,這樣就不會出現number型別精度丟失的問題。

var orderNumber = $(`.j_OrderNumber`)[0].dataset.ordernumber // `9148368244236619`

typeof(orderNumber) === `string` // true

複製程式碼

所以我們在通過自定義屬性取值number型別時,並且預期這些值會是類似訂單號這種會超過安全閾值的數值時,不要使用jquery的data方法。

大數值的精度問題

能夠被“安全”呈現的最大整數是Math.pow(2, 53) - 1,即9007199254740992。在ES6中被定義為Number.MAX_SAFE_INTEGER

在開發環境,根據程式的特殊性,在有可能出現這種情況時我們應該杜絕掉超出安全閾值的大整數,並給出友好提示:

function isSafeInteger(num) {
	return typeof(num) === `number` && num % 1 == 0 && Math.abs(num) <= Math.pow(2, 53) -1;
}
複製程式碼

在一般電商業務中比較常見出現大數值的場景也就是訂單號了,這類場景後端傳值給我們的時候都強制包裝成string型別就會解決大多數的精度丟失問題了。

至於為什麼會只有2的53次方-1的整數是安全的,可以看阮神的關於數值的文章有詳細介紹

小數的精度問題

經典的 0.1 + 0.2 === 0.3 // false 問題

0.1 + 0.2 === 0.30000000000000004

小數比較

對於這類數值比較問題,如果我們已經知道了目標比較值,即如果我們已經明確要與0.3進行比較,我們也可以不需要得到0.1+0.2的真實期望結果值(0.3),因為如果我們要得到0.3,還需要對0.1和0.2進行操作。常規解法:

(0.1 * 10 + 0.2 * 10) / 10 = 0.3
複製程式碼

在「你不知道的javascript」一書中有提到一種判斷方法,設定一個誤差範圍值,通常也稱為“機器精度”,對於javascript來說,這個值通常為Math.pow(2, -53)

我們可以將需要比較的兩個值進行相減,再與這個機器精度進行比較,如果在誤差範圍內,我們也視為兩個值是相等的。

function numbersCloseEnoughToEqual(num1, num2) {
	return Math.abs(num1 - num2) < Math.pow(2, -53);
}
複製程式碼

小數展示

對於電商業務來講,小數經過四則運算後可能會出現失去精度的問題,但是作為展示來說我們都會呼叫toFixed()進行小數後幾位的約定,呼叫了這個方法後小數失去精度的問題也就迎刃而解了,不可能出現0.30000000000000004這樣的數值。

所以在業務中有需要進行小數四則運算並會展示在頁面中,呼叫toFixed()方法!

但是toFixed()也有失去精度的時候!

1.335.toFixed(2)
// "1.33"
複製程式碼

解決辦法

function toFixed(num, s) {
    var times = Math.pow(10, s)
    var des = num * times + 0.5
    des = parseInt(des, 10) / times
    return des + ``
}
複製程式碼

小數的四則運算

參考

本文來自二口南洋,有什麼需要討論的歡迎找我。

相關文章