📋 目錄
0.1 + 0.2 !== 0.3、
Number.MAX_SAFE_INTEGER以上的數字不再精確、JSON 解析大 ID 被截斷——這三個問題是前端開發中最常見的數字地雷。這篇用實際踩坑經歷,整理所有你需要知道的預防措施和解決方案。
問題一:0.1 + 0.2 !== 0.3
這不是 bug——這是 IEEE 754 雙精度浮點數的設計特性。
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
// 解釋:0.1 和 0.2 在二進位中是無窮循環小數
// 儲存時被截斷,所以相加後出現了誤差
這個誤差的量級通常很小(~2.22e-16),但在某些場景下足以造成問題。
比較兩個浮點數是否「足夠接近」:
function roughlyEqual(a, b, epsilon = 1e-10) {
return Math.abs(a - b) < epsilon;
}
console.log(roughlyEqual(0.1 + 0.2, 0.3)); // true
console.log(Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON); // 這個不夠好
console.log(Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON * Math.max(Math.abs(0.1 + 0.2), Math.abs(0.3))); // 相對誤差
問題二:MAX_SAFE_INTEGER 以上的數字不再精確
JavaScript 的 Number 類型是 53 位精度的浮點數,超過 Number.MAX_SAFE_INTEGER(9007199254740991)的數字就會出現「相鄰整數無法區分」的問題:
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number.MAX_SAFE_INTEGER + 1); // 9007199254740992(精確)
console.log(Number.MAX_SAFE_INTEGER + 2); // 9007199254740992(錯誤!等於 +1)
console.log(Number.MAX_SAFE_INTEGER + 3); // 9007199254740994(錯誤!)
實務上踩坑的地方:
- Twitter Snowflake ID(範圍可能超過 MAX_SAFE_INTEGER)
- Blockchain 交易 hash(有時需要大數)
- 某些資料庫的 BIGINT 欄位(超過 JS Number 精度的 ID)
問題三:JSON 解析大數字被截斷
這是最容易被忽視的一個問題。當後端返回的 JSON 包含大數字時,JS 解析時就已經被截斷了:
// 後端返回
{ "orderId": 9007199254740993, "transactionId": 90071992547409923 }
// 在 JavaScript 解析時(JSON.parse):
const data = JSON.parse('{"orderId": 9007199254740993}');
console.log(data.orderId); // 9007199254740992(已經截斷!)
// 解決方案:後端應該用字串傳遞大數
// { "orderId": "9007199254740993" } // 字串
// { "orderId": { "$numberLong": "9007199254740993" } } // MongoDB 格式
正確解法:不同場景的實際方案
1. 金錢計算:整數分(cents)策略
最簡單、最安全的方案:永遠以「分」儲存,以「元」顯示。
// 錯誤
const prices = [0.1, 0.2, 0.3];
const total = prices.reduce((a, b) => a + b, 0);
console.log(total === 0.6); // false
// 正確:全部乘以 100,變成整數
const priceCents = [10, 20, 30]; // 分
const totalCents = priceCents.reduce((a, b) => a + b, 0);
// 顯示時除回去
const display = (totalCents / 100).toFixed(2); // "0.60"
2. 需要小數的金融計算:decimal.js
import Decimal from 'decimal.js';
// 過去:浮點數精度問題
console.log(new Decimal(0.1).plus(0.2).toString()); // "0.3"
// 金融計算
const amount = new Decimal('0.1')
.plus('0.2')
.times('3')
.toFixed(2); // "0.90"
// 處理大數
const bigNum = new Decimal('9007199254740993');
console.log(bigNum.plus(2).toString()); // "9007199254740995"(精確)
3. 純大整數:BigInt
// BigInt 可以處理任意大小的精確整數
const big = BigInt('9007199254740993000');
console.log(big + BigInt(2)); // 9007199254740993002n
// BigInt 的限制:不支援小數
const bad = BigInt(0.1); // TypeError
4. 顯示格式化:Intl.NumberFormat(不需要 library)
// 格式化幣種
const formatter = new Intl.NumberFormat('zh-TW', {
style: 'currency',
currency: 'TWD',
});
console.log(formatter.format(1234567.89)); // "NT$1,234,567.89"
// 格式化大數(帶分隔符)
console.log(new Intl.NumberFormat('zh-TW').format(9007199254740993));
// "9,007,199,254,740,993"
Library 選擇指南
| Library | 大小 | 特點 | 適合場景 |
|---|---|---|---|
| 不用 library | 0 KB | 整數分策略 + Intl.NumberFormat | 大多數場景 |
| decimal.js | ~25 KB | 完整小數支援、高精度配置 | 金融計算 |
| bignumber.js | ~25 KB | 更多配置選項 | 需要不同精度配置 |
| currency.js | ~2 KB | 專門為幣種設計 | 簡單的幣種計算 |
後端 API 傳大數的最佳實踐
// 後端:永遠用字串傳大數
// Express
app.get('/orders/:id', (req, res) => {
res.json({
id: String(order.id), // 用字串,不用數字
amount: order.amount_cents // 用整數分,不用 float
});
});
// 前端
const data = await fetch('/orders/123').then(r => r.json());
console.log(data.id); // "9007199254740993"(字串,精確)
實戰 Checklist
## 數字處理安全清單
### 金額相關
- [ ] 後端所有金額相關欄位用整數分(cents)儲存
- [ ] 前端顯示前用 Intl.NumberFormat 格式化
- [ ] API 請求中的金額也用整數分傳遞
- [ ] 禁止 float 用於任何金額儲存或計算
### 大數 ID 相關
- [ ] 後端返回大數 ID 時使用字串
- [ ] 前端收到字串後不轉 Number(除非必要)
- [ ] JSON.parse 時檢查是否有大數(> MAX_SAFE_INTEGER)
### 計算精度
- [ ] 需要精確小數的場景使用 decimal.js
- [ ] 需要比較浮點數時使用相對誤差比較
- [ ] 避免使用 toFixed(它本身也有精度問題)
- ❌ (0.1 + 0.2).toFixed(2) === '0.30'
- ✅ 用 Math.round(0.3 * 100) / 100
總結:記住這三件事
- 永遠不要用 float 存金額——用整數分(cents)是最簡單最安全的做法
- 後端返回大數 ID 永遠用字串——JSON.parse 的那一刻就截斷了,來不及救
- 需要精確小數計算時用 decimal.js——
Number類型不擅長這個
// 記住這個模式
const totalCents = items.reduce((sum, item) => {
const cents = Math.round(parseFloat(item.price) * 100);
return sum + cents;
}, 0);
// 顯示
new Intl.NumberFormat('zh-TW', { style: 'currency', currency: 'TWD' })
.format(totalCents / 100);
延伸閱讀
- TypeScript 6.0 RC 升級指南 — BigInt 與數字類型新特性
本文基於 FRO-84 研究資料整理,涵蓋 IEEE 754 浮點數限制、金額計算最佳實踐、BigInt 使用場景、以及 API 設計建議。