2024-03-16|閱讀時間 ‧ 約 26 分鐘

別被 0.1 + 0.2 給耍了


「在 JavaScript 中 0.1 + 02 等於多少?」

這是我在面試時會問的一題。有經驗的工程師應該知道我在問什麼,但相信仍有不少人可能還不知道 0.1 + 0.2 不等於 0.3
幾年前有位同事跑來找我求救,他維護的某個早期專案出現線上問題,投資人在網頁上看到的金額與實際相差 1 塊。在聽完他的描述後,我問他這數字是前端計算的嗎?他回答是,此時直覺告訴我,這是浮點數精度問題。


浮點數精度問題?為什麼不等於 0.3?

簡單說,浮點數精度問題是指在使用 JavaScript 進行浮點數運算時可能出現的精度失真或精度丟失的情況。而造成浮點數精度問題的根本原因是電腦內部所使用的二進制浮點數表示法。
JavaScript 使用的是 IEEE 754 標準的雙精度浮點數表示法,即 64 位元,其中一位表示符號位,11 位表示指數,剩餘的 52 位表示尾數。
儘管這個表示法可以表示大範圍的數字,但它是基於二進制的,而我們通常使用的是十進制。這就導致了某些十進制小數無法完全精準地轉換為二進制表示,進而產生精度問題。例如,十進制中的 0.1 在該二進制中是一個無窮循環小數。這種問題在進行浮點數運算時尤為突出,因為即使是簡單的運算,結果也可能存在微小的誤差。

例如,在 JavaScript 中執行如下操作:

0.1 + 0.2 // 0.30000000000000004

根據我們的直覺,答案應該是 0.3。然而,在 JavaScript 中,由於浮點數精度問題,實際上結果可能是一個非常接近 0.3 的數字,但不是精確的 0.3

另一個造成精度問題的原因是浮點數的表示範圍限制。雖然雙精度浮點數可以表示非常大和非常小的數字,但在超出表示範圍時,將會出現溢出或下溢的情況,進而導致數字的失真。

在此就不對 IEEE 754 進行更詳細的說明,畢竟其浮點數行為與背後的原理已涉及計算機領域,相較之下前端工程師需更專注的是對浮點數的警惕與應對。如果有興趣了解原理,網上也有相當多資源。


凡遇浮點數,皆小心為上

在金融應用中,由於金額可能非常小且需要高精度計算,在進行複雜的計算時可能因此產生錯誤的判斷或造成四捨五入後的結果異常。
例如,計算某商品價格乘上某手續費後的最終價格,但因為浮點數精度問題,最終價格可能會有微小的偏差。以下列出幾個前端工程師可能遇到的情況。


1. 連續計算後的累積誤差:

在進行一連串浮點數的累積計算時,可能會累積誤差,導致最終結果與預期值不同。這在複雜的算法或迴圈迭代計算中尤其顯著。

let sum = 0.1
for (let i = 0; i < 10; i++) {
sum += 0.1
}

// 預期結果應為 1,但實際結果為 1.0999999999999999
console.log(sum)


2. 比較操作中的不確定性:​

在進行浮點數的比較操作時,可能會導致不符期望的比較或進入錯誤流程。

const foo = 0.1 + 0.2
const bar = 0.3

// 預期應該執行'小於等於'情境,但實際執行為'大於'情境
if (foo > bar) {
// 大於時執行某些動作...
console.log('大於');
} else {
// 小於等於時執行另外某些動作...
console.log('小於等於');
}


3. 不同計算順序的差異:

相同的數字在需求不變的情況下,即便只是計算順序不同,也可能因誤差而造成截然不同的結果。

const sum1 = 0.1 + 0.2 + 0.3 // 0.6000000000000001
const sum2 = 0.3 + 0.2 + 0.1 // 0.6
const isEqual = sum1 === sum2

console.log(isEqual) // 預期輸出 true,但實際為 false


不是你說不就不

既然前端計算浮點數是有風險的,相信一定會有人說,重要數字的計算本就不應該由前端處理,其實這算對也不算對。
金額、匯率、報酬率,手續費等等的數字在 domain 中的確是採後端計算結果為準並以此執行。但如果需即時呈現,假使都透過 API 取得,那就可能出現使用體驗上的不流暢,特別是當使用者就是變因之一時。

例如在基金交易中,手續費的玩法相當多樣,固定金額、單一費率、級距金額、級距費率、內含或外扣,適用基金標的或投資身份的優惠費率等等。
為了即時將結果呈現在畫面上,使用者於輸入框中每輸入一次金額就得透過 API 不斷重新查詢費率與計算手續費。
如果該數字不影響後續申購流程,那我們還可以讓這段邏輯以非同步方式執行。但如果後續流程中某些欄位受到該數字影響必須等待其結果呢?這時使用體驗上就會出現卡頓,即便你已經為輸入框做過防抖也一樣。
當你無法將使用者因素排除於變因之外,而使用者又能不間斷地改變因子時,這樣的體驗可能還沒等到上線,光是在 QA 就已經被提出了。

為此我們可能會這麼做,盡可能地在使用者未察覺的情況下透過 API 預先取得部分或全部計算邏輯。當需要計算時以落地方式立即處理,直接避開後端執行,API 響應時間與使用者網速等因素。當然這只是為了前台畫面的操作體驗與呈現,實際商業邏輯運算依然是後端處理。

工程師要懂得保護好自己

也因身處金融科技的產業特性,我會要求團隊內的工程師在做金額相關計算時一定要使用指定函式做特別處理。
千萬別小看那一點點的誤差,想像在電商購物被多扣一塊錢,你可能覺得沒什麼,大不了以後不用這家。但如果是定期定額交易時每次被多買一塊錢,你可能已經拿起手機撥打客訴專線了。不想被客訴甚至被主管機關盯上,那就別嫌麻煩,多做一步好好保護自己吧。




Cheng's murmur

來台北這麼多年,
最受不了的就是冬天那種要下不下的雨。
那傘撐也不是,不撐也不是。
分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.