JavaScript 浮點數問題
JavaScript 浮點數精度問題
TL;DR0.1 + 0.2 !== 0.3 不是 JS bug,是 IEEE 754 浮點數標準 的本質限制。所有走 IEEE 754 的語言都有這問題。金錢計算永遠用 Decimal.js / BigInt + 整數位元==,別用 Number;簡單顯示用 toFixed()。
經典範例(全部踩過)
0.1 + 0.2 === 0.3 // false ⚠️
0.1 + 0.2 // 0.30000000000000004
0.1 * 3 // 0.30000000000000004
19.9 * 100 // 1989.9999999999998
(0.1 + 0.2 + 0.3) === 0.6 // false
0.3 - 0.2 // 0.09999999999999998
為什麼:電腦用 二進位浮點數 表示小數,0.1 在二進位是 無限循環(0.0001100110011...),只能截斷儲存 → 失準。
0.1 (10 進位) = 0.0001100110011001100110011001100110011001100110011...(2 進位,無限循環)
↑
IEEE 754 截斷在這附近
4 種解法
1. toFixed()(簡單顯示用)
+(0.1 + 0.2).toFixed(2) // 0.3
(0.1 + 0.2).toFixed(2) // "0.30" (string!)
注意:toFixed 回傳 string,前面加 + 或 Number() 轉回 number。
適合:UI 顯示金額、簡單比較、低精度需求 不適合:多次累積運算(精度誤差會累積)
2. 先乘後除(整數運算)
(0.1 * 10 + 0.2 * 10) / 10 // 0.3 ✅
原理:把 float 轉成 整數運算(整數沒精度問題),最後再除回去。
陷阱:乘法本身也可能爆:0.1 * 10 的結果你以為是 1,但...
0.1 * 10 // 1 ✅(湊巧 OK)
0.29 * 100 // 28.999999999999996 ⚠️
所以只在 1-2 位小數 時可靠,複雜情況仍要用 Decimal.js。
3. Decimal.js / Big.js(推薦)
npm install decimal.js
import Decimal from 'decimal.js';
new Decimal(0.1).plus(0.2).toNumber() // 0.3 ✅
new Decimal('0.1').plus('0.2').toString() // '0.3'
// 鏈式運算
new Decimal(100).times(0.29).toNumber() // 29 ✅
選擇:
| 套件 | 大小 | 特色 |
|---|---|---|
| decimal.js | ~30KB | 任意精度、一般場景首選 |
| decimal.js-light | ~12KB | 精簡版 |
| big.js | ~6KB | 最輕,API 較陽春 |
| bignumber.js | ~30KB | 跟 decimal.js 同作者,API 略不同 |
金融計算強制用任何處理金錢、稅務、計費的程式碼,別用 Number,直接 Decimal.js。一次省 N 次客訴。
4. Number.EPSILON(僅用於比較)
function isFloatEqual(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}
isFloatEqual(0.1 + 0.2, 0.3) // true ✅
Number.EPSILON ≈ 2.22e-16,是 Number 的最小可分辨差距。只解決「比較」,不解決運算後的儲存值仍然是 0.30000...004。
進階解法:整數位元儲存
銀行 / 券商 / 大型電商常用:
// 用「分」當單位,避免小數
const priceInCents = 1099; // $10.99
const taxInCents = 88;
const totalCents = priceInCents + taxInCents; // 1187
const display = (totalCents / 100).toFixed(2); // "11.87"
規則:
- DB 存整數(以最小單位 — 分 / 厘)
- 所有運算 都用整數
- 只在 顯示時 除回小數
特別重要:對接金融 API(Stripe / 台灣金流)規範就是用最小單位,你的內部結構配合最省事。
// Stripe API 的 amount 是 cents
stripe.charges.create({
amount: 2000, // $20.00 = 2000 cents
currency: 'usd',
});
BigInt(整數但任意大)
const big = 9007199254740993n; // 結尾 n
big + 1n // 9007199254740994n
Number.MAX_SAFE_INTEGER // 9007199254740991
// 不能跟 Number 直接運算
1n + 1 // ❌ TypeError
1n + BigInt(1) // 2n ✅
BigInt 解的是「超過 Number 安全範圍的大整數」(> 2^53),不是浮點精度。但配合「整數位元儲存」可以處理超大金額(虛擬貨幣 / 衛星測量等)。
各情境決策
| 情境 | 用什麼 |
|---|---|
| 顯示 UI 金額(已從 server 取得) | toFixed(2) |
| 計算購物車總額 | Decimal.js |
| 金融 / 計費 | 整數位元 + Decimal.js |
| 累加大量百分比 | Decimal.js |
| Number 比較 | Number.EPSILON |
| 超大整數(如使用者 ID、訂單編號) | BigInt |
其他語言也有同問題
不是 JS 獨有,任何 IEEE 754 的語言都會踩:
# Python
0.1 + 0.2 == 0.3 # False
print(0.1 + 0.2) # 0.30000000000000004
// Java
double a = 0.1 + 0.2;
System.out.println(a == 0.3); // false
// 解法:BigDecimal
import java.math.BigDecimal;
BigDecimal x = new BigDecimal("0.1").add(new BigDecimal("0.2"));
// x = 0.3 精確
Java 的 BigDecimal、Python 的 Decimal、JS 的 Decimal.js — 都是同一思路。
實際 React 範例(縮放比例)
// ❌ 浮點累加,N 次後誤差爆炸
const [scale, setScale] = useState(1);
const handleZoom = () => setScale(prev => prev + 0.1);
// ✅ 用 Decimal.js
import Decimal from 'decimal.js';
const handleZoom = () => {
setScale(prev => new Decimal(prev).plus(0.1).toNumber());
};
// ✅ 或先放大 100 倍當整數運算
const [scaleX100, setScaleX100] = useState(100);
const handleZoom = () => setScaleX100(prev => prev + 10);
const displayScale = scaleX100 / 100;
何時可以裝沒看到
不必每次都用 Decimal如果你的 誤差容忍度大於 1e-10(顯示 2 位小數的 UI、CSS animation、Game scaling),直接用 Number 就好。 只在以下場景 必須處理:
- 金錢、稅務、計費
- 累加 / 累乘 100+ 次(誤差會累積)
- 跟其他系統(API、DB)交換精確數字