本文 AI 產出,尚未審核

JavaScript 數學與數字處理:精確運算策略(BigInt、Decimal)


簡介

在日常的前端與 Node.js 開發中,我們常常只用 Number 來處理所有的數值計算。
然而 Number 是 IEEE‑754 雙精度浮點數,在表示極大、極小或是需要「精確」小數的情境時,會出現捨入誤差或是超出安全範圍的問題。

當應用牽涉到 金額計算、加密貨幣、科學運算、時間戳記 等需要 絕對精度 的領域時,單純依賴 Number 會帶來隱藏的風險。
ECMAScript 2020 引入了 BigInt,而社群也提供了 Decimal(或 Decimal.js)等方案,讓開發者可以在 JavaScript 中安全、正確地完成「大數」與「高精度小數」的運算。

本篇文章將從概念說明、實作範例、常見陷阱到最佳實踐,完整呈現在 JavaScript 中使用 BigIntDecimal 進行精確運算的全貌,幫助讀者從初學者順利過渡到中級開發者。


核心概念

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
  • 不可混用BigIntNumber 必須透過顯式轉換後才能比較或運算。
  • 支援的運算子+ - * / % ** << >> & | ^ ~(除法會捨去小數部分)。

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.jsbig.jsbignumber.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 個實務範例,展示如何在不同情境下選擇 BigIntDecimal

範例 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 * 3Number 中會得到 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 處理,避免精度損失。


常見陷阱與最佳實踐

陷阱 說明 解法
混用 NumberBigInt 直接比較或相加會拋出 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. 統一型別:在同一模組內盡量保持 BigIntDecimal 的一致性,避免頻繁轉型。
  3. 使用字串建立:無論是 BigInt 還是 Decimal,建議以字串作為建構子參數,保證不會在中間步驟失去精度。
  4. 設定全域精度:對於 Decimal,一次設定即可,避免在每次運算時重複指定。
  5. 測試邊界值:特別是 Number.MAX_SAFE_INTEGERNumber.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 為代表)提供 十進位高精度,解決金額、科學計算等「小數」精度問題。
  • 正確的使用方式是 先評估需求 → 選擇合適類型 → 統一型別 → 設定適當精度 → 撰寫測試

掌握了 BigIntDecimal 的概念與實作,你就能在 JavaScript 中自信地處理所有需要精確的數學運算,讓前端或後端應用在金融、區塊鏈、科學模擬等高風險領域也能保持正確可靠。祝開發順利!