JavaScript 數學與數字處理:精確運算策略(BigInt、Decimal)
簡介
在日常的前端與 Node.js 開發中,我們常常只用 Number 來處理所有的數值計算。
然而 Number 是 IEEE‑754 雙精度浮點數,在表示極大、極小或是需要「精確」小數的情境時,會出現捨入誤差或是超出安全範圍的問題。
當應用牽涉到 金額計算、加密貨幣、科學運算、時間戳記 等需要 絕對精度 的領域時,單純依賴 Number 會帶來隱藏的風險。
ECMAScript 2020 引入了 BigInt,而社群也提供了 Decimal(或 Decimal.js)等方案,讓開發者可以在 JavaScript 中安全、正確地完成「大數」與「高精度小數」的運算。
本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,完整呈現在 JavaScript 中使用 BigInt 與 Decimal 進行精確運算的全貌,幫助讀者從初學者順利過渡到中級開發者。
核心概念
1. 為什麼 Number 不是萬能的?
| 特性 | Number(IEEE‑754) |
可能的問題 |
|---|---|---|
| 表示範圍 | ±1.797 × 10³⁰⁸ | 超過 Number.MAX_SAFE_INTEGER(9,007,199,254,740,991)時,相鄰整數無法區分 |
| 小數精度 | 約 15-17 位十進位有效數字 | 0.1 + 0.2 ≠ 0.3,捨入誤差 |
| 整除/取餘 | 使用浮點運算 | 可能得到非整數的餘數 |
結論:如果你的程式只需要「近似」的結果,
Number足夠;但若需絕對正確的整數或小數,必須另尋方案。
2. BigInt:任意長度的整數
2.1 基本語法
// 直接在數字後加上 n 表示 BigInt
const big = 123456789012345678901234567890n;
// 由字串或 Number 轉換
const fromString = BigInt("98765432109876543210");
const fromNumber = BigInt(42); // 仍是 42n
- 不可混用:
BigInt與Number必須透過顯式轉換後才能比較或運算。 - 支援的運算子:
+ - * / % ** << >> & | ^ ~(除法會捨去小數部分)。
2.2 與 Number 的互換
const big = 9007199254740993n; // 超過安全整數
const num = Number(big); // 轉成 Number,失去精度
console.log(num); // 9007199254740992
// 正確的比較方式
if (big > 9007199254740992n) {
console.log('big 確實更大');
}
提醒:
Number()只能在BigInt落在安全範圍內時保證正確;否則會出現捨入。
3. Decimal(高精度小數)
JavaScript 標準尚未提供原生的十進位浮點類型,但社群的 decimal.js、big.js、bignumber.js 等套件已廣泛採用。以下以 decimal.js 為例說明。
3.1 安裝與引入
npm install decimal.js
// Node.js
const Decimal = require('decimal.js');
// ES Module
import Decimal from 'decimal.js';
3.2 基本使用
const a = new Decimal('0.1');
const b = new Decimal('0.2');
const sum = a.plus(b); // a + b
console.log(sum.toString()); // "0.3"
Decimal內部以 字串 或 BigInt 保存精度,避免二進位浮點的誤差。- 常用方法:
.plus()、.minus()、.times()、.div()、.pow()、.sqrt()。
3.3 設定全域精度
Decimal.set({ precision: 30 }); // 設定 30 位有效數字
const x = new Decimal('1').div('3');
console.log(x.toString()); // "0.333333333333333333333333333333"
4. 程式碼範例
以下提供 5 個實務範例,展示如何在不同情境下選擇 BigInt 或 Decimal。
範例 1️⃣ 大數指紋(加密)
// 假設要處理 256 位元的雜湊值
const hashHex = 'f3a1b2c4d5e6f7890a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8090a1b2c3d4';
const hashBigInt = BigInt('0x' + hashHex); // 直接以十六進位字串建立
console.log(hashBigInt.toString(10)); // 十進位大數字串
使用技巧:直接以十六進位字串轉成
BigInt,可避免手動切割或使用第三方套件。
範例 2️⃣ 金額計算(台幣)
import Decimal from 'decimal.js';
// 設定金額精度到分(2 位小數)
Decimal.set({ precision: 20 });
const price = new Decimal('1999.99'); // 商品單價
const qty = new Decimal('3');
const taxRate = new Decimal('0.05'); // 5% 稅
const subtotal = price.times(qty); // 1999.99 * 3
const tax = subtotal.times(taxRate);
const total = subtotal.plus(tax);
console.log('小計:', subtotal.toFixed(2)); // "5999.97"
console.log('稅金:', tax.toFixed(2)); // "300.00"
console.log('總計:', total.toFixed(2)); // "6299.97"
為什麼不用
Number?1999.99 * 3在Number中會得到 5999.969999999999,最終金額會多出 0.01 元的誤差。
範例 3️⃣ 時間戳記的毫秒加減(BigInt)
// Unix epoch 毫秒(2025-01-01T00:00:00Z)
const start = 1735689600000n; // 使用 BigInt
// 加上一天的毫秒(24 * 60 * 60 * 1000)
const oneDay = 86400000n;
const nextDay = start + oneDay;
console.log(String(nextDay)); // "1735776000000"
注意:如果使用
Number,在極大時間戳記(如超過 2^53)會失去毫秒精度。
範例 4️⃣ 科學計算:π 的高精度
import Decimal from 'decimal.js';
// 設定 50 位有效數字
Decimal.set({ precision: 60 });
function arctan(x) {
// 使用泰勒級數計算 arctan(x) ≈ Σ (-1)^n * x^(2n+1)/(2n+1)
let sum = new Decimal(0);
let term = new Decimal(x);
let n = 0;
while (!term.isZero()) {
sum = sum.plus(term);
n++;
term = term.times(x.pow(2)).negated().dividedBy(2 * n + 1);
// 當 term 小於 1e-55 時可視為 0
if (term.abs().lt('1e-55')) break;
}
return sum;
}
// Machin formula: π/4 = 4*arctan(1/5) - arctan(1/239)
const pi = arctan(new Decimal(1).div(5)).times(4)
.minus(arctan(new Decimal(1).div(239))).times(4);
console.log('π ≈', pi.toString());
示範:使用
Decimal可以在不依賴內建Math.PI的情況下自行計算高精度 π,適合數學或加密演算法的驗證。
範例 5️⃣ 混合運算:BigInt + Decimal(實務橋接)
import Decimal from 'decimal.js';
// 假設有一筆大額交易的金額(單位:分),使用 BigInt 儲存
const amountInCents = 12345678901234567890n;
// 需要將金額轉成新台幣(除以 100)並加上 2% 手續費
Decimal.set({ precision: 30 });
const amountDecimal = new Decimal(amountInCents.toString()); // 轉成 Decimal
const amountNTD = amountDecimal.div(100);
const fee = amountNTD.times(0.02);
const total = amountNTD.plus(fee);
console.log('原金額 (NT$):', amountNTD.toFixed(2));
console.log('手續費 (NT$):', fee.toFixed(2));
console.log('總金額 (NT$):', total.toFixed(2));
關鍵:
BigInt只能直接與BigInt互動,若要與小數運算,先 轉成字串 再交給Decimal處理,避免精度損失。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解法 |
|---|---|---|
混用 Number 與 BigInt |
直接比較或相加會拋出 TypeError。 |
先用 BigInt() 或 Number() 轉型,或保持同一類型。 |
Number() 轉 BigInt 超出安全範圍 |
會失去精度,結果不可靠。 | 只在安全範圍內使用;若不確定,直接以字串建立 BigInt。 |
Decimal 設定不當 |
精度太低會仍出現捨入誤差。 | 適當設定 precision(建議 20+ 位),或根據業務需求調整。 |
忘記使用 .toString() 或 .toFixed() 輸出 |
直接 console.log 會顯示 Decimal 物件而非數值。 |
使用 toString()、toNumber()、toFixed() 取得字串或 Number。 |
除法捨去小數(BigInt) |
BigInt 的除法會自動向零捨去,可能不是期望結果。 |
若需要小數,先轉成 Decimal 或使用 Number。 |
| 性能問題 | Decimal 物件較重,迴圈大量運算會變慢。 |
僅在需要高精度的關鍵區段使用;大量簡單運算仍可使用 Number。 |
最佳實踐
- 先判斷需求:是否真的需要「絕對」精度?若只是顯示或暫時計算,可暫時使用
Number。 - 統一型別:在同一模組內盡量保持
BigInt或Decimal的一致性,避免頻繁轉型。 - 使用字串建立:無論是
BigInt還是Decimal,建議以字串作為建構子參數,保證不會在中間步驟失去精度。 - 設定全域精度:對於
Decimal,一次設定即可,避免在每次運算時重複指定。 - 測試邊界值:特別是
Number.MAX_SAFE_INTEGER、Number.MIN_SAFE_INTEGER、以及極小的Decimal值,寫單元測試確保不會因捨入而出錯。
實際應用場景
| 場景 | 為何需要精確運算 | 建議工具 |
|---|---|---|
| 金融交易(銀行、支付平台) | 金額必須到分(或更細)且不可有誤差 | Decimal(或 big.js) |
| 區塊鏈 / 加密貨幣 | 代幣單位常是 10⁻⁸ 甚至更小,且總供給可能超過 Number.MAX_SAFE_INTEGER |
BigInt + Decimal(用於比例) |
| 科學計算(天文、物理模擬) | 常涉及極大/極小的指數,捨入會累積誤差 | Decimal(高精度)或 BigInt(計算指數) |
| 資料庫主鍵(自增 ID) | 超過 2³¹‑1 時,MySQL、PostgreSQL 仍支援 64 位元整數 | BigInt(直接映射) |
| 時間戳記與日曆運算 | 毫秒級精度在遠古或未來時間點會超出安全範圍 | BigInt(毫秒或微秒) |
總結
Number雖然便利,但在 大數、高精度小數、時間戳記 等情境下會產生不可接受的誤差。BigInt為 任意長度的整數,適合金融、區塊鏈、資料庫主鍵等需要「整數」精確度的場景。Decimal(以decimal.js為代表)提供 十進位高精度,解決金額、科學計算等「小數」精度問題。- 正確的使用方式是 先評估需求 → 選擇合適類型 → 統一型別 → 設定適當精度 → 撰寫測試。
掌握了 BigInt 與 Decimal 的概念與實作,你就能在 JavaScript 中自信地處理所有需要精確的數學運算,讓前端或後端應用在金融、區塊鏈、科學模擬等高風險領域也能保持正確與可靠。祝開發順利!