本文 AI 產出,尚未審核

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。

最佳實踐

  1. 先判斷需求:是否真的需要「絕對」精度?若只是顯示或暫時計算,可暫時使用 Number。
  2. 統一型別:在同一模組內盡量保持 BigInt 或 Decimal 的一致性,避免頻繁轉型。
  3. 使用字串建立:無論是 BigInt 還是 Decimal,建議以字串作為建構子參數,保證不會在中間步驟失去精度。
  4. 設定全域精度:對於 Decimal,一次設定即可,避免在每次運算時重複指定。
  5. 測試邊界值:特別是 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 中自信地處理所有需要精確的數學運算,讓前端或後端應用在金融、區塊鏈、科學模擬等高風險領域也能保持正確與可靠。祝開發順利!