📋 目錄
如果你的應用涉及任何形式的數值計算——電商定價、加密貨幣錢包、財務儀表板——你一定踩過浮點數的坑。
0.1 + 0.2 !== 0.3、Number.MAX_SAFE_INTEGER以上的 ID 被截斷——這些問題不是 bug,而是 JavaScript 採用的 IEEE 754 浮點數標準的根本限制。2026 年,這個領域有幾個重要的新發展:Math.sumPrecise 已經到了 Stage 4(Firefox 已實作)、BigDecimal 提案進入了 Stage 1、 WASM 高精度方案越來越成熟。這篇文章是「前端數字地雷」的進階版,專注於 2026 年的新發展與實戰場景。
關於浮點數基礎、BigInt 基本用法、decimal.js 入門,建議先參考 前端數字地雷:JavaScript 浮點數精度與大數處理的正確解法。
2026 年數值計算的整體圖景
在 2026 年,前端數值計算的生態已經明顯分层:
| 層次 | 場景 | 解決方案 |
|---|---|---|
| 原生基礎 | 簡單加總、大整數 ID | Math.sumPrecise(新)、BigInt |
| 庫支撐 | 電商、金融、科學計算 | decimal.js、bignumber.js |
| WASM 高效能 | 大量資料、高精度需求 | MPFR.js |
| 未來期待 | 原生高精度小數 | BigDecimal(Stage 1) |
選擇哪個層次的方案,取決於你的具體場景。
Math.sumPrecise:Stage 4 的精確加總
什麼是 Math.sumPrecise
Math.sumPrecise 是 TC39 提出的新提案,專門解決多個浮點數相加時的累積誤差問題。
// 過去的問題
0.1 + 0.2 + 0.3 + 0.4 + 0.5; // 0.1 + 0.2 = 0.30000000000000004
// Math.sumPrecise 的解決
Math.sumPrecise(0.1, 0.2, 0.3, 0.4, 0.5); // 1.5(精確)
// 也支援陣列
Math.sumPrecise([0.1, 0.2, 0.3, 0.4, 0.5]); // 1.5
當前支援狀態
| 瀏覽器 | 支援 |
|---|---|
| Firefox | ✅ 已實作 |
| Chrome | 🔜 即將支援 |
| Safari | 🔜 即將支援 |
| Node.js | ✅ v22+ |
// Fallback 模式
function safeSum(...numbers) {
if (typeof Math.sumPrecise === 'function') {
return Math.sumPrecise(...numbers);
}
// 退回到整數法
const sum = numbers.reduce((acc, n) => {
const [int, dec] = n.toString().split('.');
const decimals = dec ? dec.length : 0;
const multiplier = 10 ** decimals;
return acc + n * multiplier;
}, 0);
return sum / 10000; // 需要更好的方式
}
適用場景與限制
// ✅ 適用:多個小數加總
const subtotal = Math.sumPrecise(
item1.price,
item2.price,
item3.price,
discount
);
// ❌ 不適用:需要減法、除法、乘法
const total = subtotal * 1.05; // 仍然會有浮點誤差
const withTax = total / 1.05; // 除法誤差仍存在
Math.sumPrecise 只解決「加總」的精度問題,不是萬靈丹。
三大實戰場景的完整方案
場景一:電商定價
問題:折扣、稅金、運費的組合計算,每一步都可能產生浮點誤差。
import Decimal from 'decimal.js';
class PricingCalculator {
constructor(currency = 'TWD') {
this.currency = currency;
}
// 計算折扣後價格
applyDiscount(price, discountPercent) {
return new Decimal(price)
.times(discountPercent)
.dividedBy(100)
.toFixed(2);
}
// 計算含稅價格
addTax(price, taxRate = 5) {
return new Decimal(price)
.times(taxRate)
.dividedBy(100)
.plus(price)
.toFixed(2);
}
// 計算訂單總價(避免浮點誤差)
calculateOrderTotal(items, discountPercent, taxRate = 5) {
const subtotal = items.reduce(
(sum, item) => sum.plus(new Decimal(item.price).times(item.quantity)),
new Decimal(0)
);
const discountAmount = subtotal
.times(discountPercent)
.dividedBy(100);
const afterDiscount = subtotal.minus(discountAmount);
const tax = afterDiscount.times(taxRate).dividedBy(100);
const total = afterDiscount.plus(tax);
return {
subtotal: subtotal.toFixed(2),
discount: discountAmount.toFixed(2),
tax: tax.toFixed(2),
total: total.toFixed(2),
};
}
}
// 使用範例
const calculator = new PricingCalculator('TWD');
const order = calculator.calculateOrderTotal([
{ price: '299.00', quantity: 3 },
{ price: '599.00', quantity: 1 },
{ price: '149.00', quantity: 2 },
], 10, 5); // 10% 折扣, 5% 稅
console.log(order);
// { subtotal: '1793.00', discount: '179.30', tax: '80.69', total: '1694.39' }
關鍵:所有的價格都作為字串傳入 Decimal.js,避免 JavaScript 浮點數在傳入時就失去精度。
場景二:加密貨幣錢包
問題:加密貨幣的數量可能非常大(小數位極多),且涉及除法(分割代幣)。
import { BigNumber, ethers } from 'ethers';
// EVM 鏈上的代幣金額通常以最小單位(wei, satoshi)存儲
const formatTokenAmount = (rawAmount, decimals = 18) => {
const bn = BigNumber.from(rawAmount);
const divisor = BigNumber.from(10).pow(decimals);
const integerPart = bn.div(divisor);
const fractionalPart = bn.mod(divisor);
// 格式化為人類可讀的數字
const fractionalStr = fractionalPart.toString().padStart(decimals, '0');
const formattedFraction = fractionalStr.slice(0, 6); // 只顯示 6 位
return `${integerPart.toString()}.${formattedFraction}`;
};
// 轉帳時,確認餘額足夠
const transfer = async (from, to, amount) => {
const decimals = 18;
const parsedAmount = ethers.utils.parseUnits(amount, decimals);
const balance = await provider.getBalance(from);
const gasEstimate = await provider.estimateGas({ from, to, value: parsedAmount });
if (balance.lt(parsedAmount.add(gasEstimate))) {
throw new Error('餘額不足');
}
return signer.sendTransaction({ to, value: parsedAmount });
};
關鍵:區塊鏈上的代幣全部以整數存儲(最小單位),只有在展示時才轉換為小數。
場景三:財務儀表板
問題:財務數據需要嚴格的精度控制,且經常需要對齊顯示(小數位數一致)。
import Decimal from 'decimal.js';
import 'intl'; // Intl.NumberFormat
class FinancialDashboard {
constructor(locale = 'zh-TW', currency = 'USD') {
this.locale = locale;
this.currency = currency;
this.formatter = new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
}
// 精確計算投資回報率
calculateROI(initialInvestment, finalValue) {
const initial = new Decimal(initialInvestment);
const final = new Decimal(finalValue);
const roi = final
.minus(initial)
.dividedBy(initial)
.times(100);
return {
percentage: roi.toFixed(2) + '%',
absolute: this.formatCurrency(final.minus(initial).toFixed(2)),
};
}
// 計算內部報酬率(IRR)
calculateIRR(cashFlows, guess = 0.1) {
const flows = cashFlows.map(cf => new Decimal(cf));
let rate = new Decimal(guess);
// Newton-Raphson 迭代
for (let i = 0; i < 100; i++) {
let npv = new Decimal(0);
let derivative = new Decimal(0);
flows.forEach((cf, t) => {
const factor = rate.plus(1).pow(t);
npv = npv.plus(cf.dividedBy(factor));
derivative = derivative.minus(
cf.times(t).dividedBy(factor.times(rate.plus(1)))
);
});
if (npv.abs().lt(0.01)) break; // 收斂
rate = rate.minus(npv.dividedBy(derivative));
}
return rate.times(100).toFixed(4) + '%';
}
formatCurrency(amount) {
return this.formatter.format(amount);
}
}
2026 Library 完整比較
| Library | 大小(min) | 小數支援 | BigInt 支援 | 維護狀態 | 適合場景 |
|---|---|---|---|---|---|
| decimal.js | ~50KB | 完整 | ❌ | 活躍 | 金融、電商 |
| bignumber.js | ~25KB | 完整 | ✅ | 活躍 | 效能敏感場景 |
| big.js | ~5KB | 完整 | ❌ | 輕量 | 簡單計算 |
| Fraction.js | ~20KB | 分數 | ❌ | 少量 | 科學計算 |
| currency.js | ~2KB | 專用 | ❌ | 少量 | 僅貨幣計算 |
選擇決策樹
你的場景需要小數嗎?
├── 否 → BigInt(原生,無依賴)
└── 是 → 你的場景是金融/電商嗎?
├── 是 → decimal.js
└── 否 → 需要 BigInt 互通嗎?
├── 是 → bignumber.js
└── 否 → big.js(最小)
WASM 高精度計算:MPFR.js
對於高效能、高精度需求的場景(如科學計算、圖形處理),純 JavaScript 的 library 可能不夠。
// MPFR.js - Multiple Precision Floating-Point Reliability
import { MPFR } from 'mpfrjs';
const a = new MPFR('0.1');
const b = new MPFR('0.2');
const sum = a.add(b);
console.log(sum.toString()); // '0.3'(精確)
// 效能對比
console.time('decimal.js');
for (let i = 0; i < 10000; i++) {
new Decimal('0.1').plus(new Decimal('0.2'));
}
console.timeEnd('decimal.js'); // ~50ms
console.time('MPFR.js');
for (let i = 0; i < 10000; i++) {
new MPFR('0.1').add(new MPFR('0.2'));
}
console.timeEnd('MPFR.js'); // ~5ms(10x 更快)
取捨:MPFR.js 的效能是 decimal.js 的 10 倍,但:
- Bundle 大小增加(~1MB WASM)
- 初始載入時間更長
- API 更複雜
常見錯誤速查表
| 錯誤寫法 | 正確寫法 | 原因 |
|---|---|---|
price * 100 | new Decimal(price).times(100) | 浮點乘法有誤差 |
0.1 + 0.2 === 0.3 | new Decimal(0.1).plus(0.2).equals(0.3) | 浮點相等性不可靠 |
parseFloat((a / b).toFixed(2)) | new Decimal(a).dividedBy(b).toFixed(2) | toFixed 是字串方法 |
BigInt(0.1) | new Decimal(0.1).toFixed(0) | BigInt 不支援小數 |
金額存成 number | 金額存成 string 或 Decimal | 避免 JSON 序列化精度丟失 |
未來展望:BigDecimal Stage 1
JavaScript 官方終於要迎來原生的高精度小數類型了。
// 提案語法(Stage 1)
const price = BigDecimal('29.99');
const tax = price * BigDecimal('0.05');
const total = price + tax;
// 或使用靜態工廠
const total = BigDecimal.add(price, tax);
與現有方案的比較:
| 維度 | BigDecimal(未來) | decimal.js(現在) |
|---|---|---|
| Bundle 負擔 | 0(原生) | ~50KB |
| API 成熟度 | 待定 | 成熟 |
| 小數精度 | 完整 | 完整 |
| 瀏覽器支援 | 未來 | 全部 |
時間線預測:
- 2026 年:Stage 2 或 Stage 3
- 2027-2028 年:Stage 4,全面支援
在那之前,decimal.js 仍然是金融和電商場景的首選。
結語:選擇正確的工具
2026 年的前端數值計算,已經有了清晰的工具層次:
- 大整數 ID:直接用
BigInt(原生,無依賴) - 簡單加總:
Math.sumPrecise(Stage 4,Firefox 已支援) - 電商/金融:decimal.js(成熟、功能完整)
- 高效能科學計算:MPFR.js(WASM)
- 僅展示格式化:
Intl.NumberFormat(原生)
沒有銀彈——根據你的場景選擇正確的工具,才是避免數值精度問題的關鍵。
關於浮點數的基礎知識和 BigInt 的基本用法,請參考 前端數字地雷:JavaScript 浮點數精度與大數處理的正確解法。
本文是「2026 前端實戰」系列文章之一。