本文 AI 產出,尚未審核

JavaScript 課程 – 數學與數字處理(Math & Numbers)

主題:浮點數誤差


簡介

在日常的前端開發或 Node.js 後端程式中,數字運算是不可避免的。看似簡單的加、減、乘、除,卻常常因為電腦使用 IEEE‑754 雙精度浮點數 來表示小數,而產生意料之外的誤差。這些誤差在金額計算、統計分析、圖形繪製等實務情境裡,若未加以處理,可能會導致 錯誤的結果、使用者信任下降,甚至法律風險

本篇文章將從 概念、常見陷阱、實用技巧 三個層面說明 JavaScript 的浮點數誤差,提供 可直接套用的程式碼範例,幫助讀者在開發時預防與修正這類問題,提升程式的可靠性與可維護性。


核心概念

1. 為什麼會有浮點數誤差?

JavaScript 只提供一種數值類型:Number,其底層遵循 IEEE‑754 雙精度(64 位元)規格。這個規格把實數映射成二進位的有限位元,不是所有十進位小數都能精確表示,例如 0.1 在二進位中是無限循環的:

0.1  →  0.0001100110011… (無限循環)

當 JavaScript 把這個無限循環截斷成 64 位元後,就會產生近似值,進一步的算術運算會把誤差累積,導致最終結果偏離我們預期的值。

2. 瀏覽器與 Node.js 的實作差異

大多數瀏覽器與 Node.js 都使用相同的 V8 引擎,浮點運算的行為相當一致。但在跨平台或跨語言(例如與後端 Java、Python)交換數據時,若不注意誤差的範圍,可能會出現「前端算 0.30000000000000004,後端卻是 0.3」的情況。

3. 如何觀察誤差?

最直接的方式是使用 ===== 比較有問題的結果:

console.log(0.1 + 0.2 === 0.3); // false
console.log(0.1 + 0.2);         // 0.30000000000000004

Tip:若要檢查「兩個浮點數是否足夠接近」,應使用 容差 (epsilon) 檢查,而非直接相等。


程式碼範例

以下範例展示常見的浮點數問題以及對策,請自行在瀏覽器或 Node.js 環境中執行觀察。

範例 1 – 基本加法誤差

// 0.1 + 0.2 之所以不等於 0.3,是因為二進位表示的誤差
const a = 0.1;
const b = 0.2;
const sum = a + b;

console.log('sum =', sum);                 // 0.30000000000000004
console.log('sum === 0.3 ?', sum === 0.3); // false

說明sum 其實是 0.30000000000000004,與 0.3 差了 4e-16。


範例 2 – 使用容差比較

/**
 * 判斷兩個浮點數是否相等(允許誤差範圍)
 * @param {number} x
 * @param {number} y
 * @param {number} [epsilon=Number.EPSILON]  // 預設為機器最小可辨識差距
 * @returns {boolean}
 */
function floatEquals(x, y, epsilon = Number.EPSILON) {
  return Math.abs(x - y) < epsilon;
}

console.log(floatEquals(0.1 + 0.2, 0.3)); // true

說明Number.EPSILON 約為 2.220446049250313e-16,足以容納大多數常見誤差。


範例 3 – 乘法與除法的誤差

const result = 0.3 * 3;   // 0.8999999999999999
const divided = 0.3 / 0.1; // 2.9999999999999996

console.log('0.3 * 3 =', result);
console.log('0.3 / 0.1 =', divided);

解法:可以先把小數轉成整數(乘以 10 的次方),再做運算,最後再除回去。

function multiplyPrecise(x, y, factor = 1e12) {
  return Math.round(x * factor) * Math.round(y * factor) / (factor * factor);
}

function dividePrecise(x, y, factor = 1e12) {
  return Math.round(x * factor) / Math.round(y * factor);
}

console.log(multiplyPrecise(0.3, 3)); // 0.9
console.log(dividePrecise(0.3, 0.1)); // 3

範例 4 – 金額計算的安全做法(使用 BigInt 或 Decimal.js)

// 方法一:以「分」為單位,使用整數計算
const price = 1999;          // 19.99 元 → 1999 分
const quantity = 3;
const totalCents = price * quantity; // 5997 分
console.log('總金額 (元) =', (totalCents / 100).toFixed(2)); // 59.97

// 方法二:使用第三方 Decimal 套件(npm i decimal.js)
const Decimal = require('decimal.js');
const dPrice = new Decimal('19.99');
const dQty   = new Decimal(3);
const dTotal = dPrice.mul(dQty);
console.log('Decimal 總金額 =', dTotal.toFixed(2)); // 59.97

說明:把金額轉成最小貨幣單位(分)或使用高精度十進位類庫,可徹底避免浮點誤差。


範例 5 – 使用 toFixedNumber.EPSILON 進行四捨五入

function roundTo(num, digits) {
  const factor = 10 ** digits;
  // 先加上一個微小的 epsilon,避免 1.005 變成 1.004999…
  return Math.round((num + Number.EPSILON) * factor) / factor;
}

console.log(roundTo(1.005, 2)); // 1.01
console.log(roundTo(0.1 + 0.2, 2)); // 0.3

說明Number.EPSILON 在四捨五入時扮演「微調」的角色,讓結果更符合直覺。


常見陷阱與最佳實踐

陷阱 可能後果 建議的解決方式
直接使用 === 比較浮點數 判斷失敗、邏輯錯誤 使用 容差比較floatEquals
金額或計數以 Number 直接相加 產生 0.30000000000000004 等錯誤 轉為整數(分、毫秒)或使用 Decimal.js
toFixed 前未加 Number.EPSILON 四捨五入結果不正確 加上 epsilon 再四捨五入
大數相乘或除後直接使用 Math.round 失去精度 先放大為整數,運算後再縮小
依賴 parseFloat 直接解析字串 解析過程中可能產生誤差 若需要精確計算,使用 big.js / decimal.js 之類的類庫

其他實用技巧

  1. 封裝常用函式:將容差比較、精確四捨五入等功能抽成工具函式,統一管理。
  2. 單元測試:針對金額、時間長度等關鍵計算寫測試,確保在不同環境、不同輸入下不會出錯。
  3. 避免不必要的浮點運算:例如在迴圈中累加 0.1,可以改為累加 1 再除以 10

實際應用場景

  1. 電商結帳

    • 需精確計算每筆商品價格、稅金、折扣。若直接使用 Number,可能出現 99.99999999999999,導致結帳金額與後端不符。解法:以「分」為單位或使用 Decimal。
  2. 圖形與動畫

    • Canvas、WebGL 位置或縮放值常以小數表示。累積偏差會造成畫面抖動或物件漂移。常見做法是每幀重新計算基準值,或使用 矩陣運算庫 內建的高精度處理。
  3. 統計與機器學習前處理

    • 大量資料的均值、標準差計算若直接使用 Number,誤差會在迭代過程中放大。可採用 Kahan 求和演算法 來降低累積誤差。
  4. 時間戳與計時器

    • Date.now() 回傳毫秒整數,若再做除以 1000 產生秒的浮點數,建議保留毫秒或使用 BigInt 處理。
  5. 金融模型

    • 利率、複利計算需要高精度。使用 big.jsdecimal.jsBigInt(配合自訂小數位)是業界慣例。

總結

  • JavaScript 的 Number 受限於 IEEE‑754 雙精度規格,無法精確表示所有十進位小數,因此在加、減、乘、除等運算時會產生微小誤差。
  • 容差比較、放大為整數、使用高精度類庫(Decimal.js、big.js) 等技巧能有效避免或修正這些問題。
  • 金額、時間、統計、圖形 等對精度敏感的領域,務必採取上述最佳實踐,並加入單元測試以防止回歸。

掌握了浮點數誤差的本質與解決方案後,你的 JavaScript 程式碼將變得更加 可靠、可預測,也能在實務專案中降低因數值錯誤帶來的風險。祝你開發愉快!