📋 目錄
這是 TC39 新語法深度解析系列的第十一篇。先問一個問題:
[1e20, 0.1, -1e20].reduce((a, b) => a + b, 0)等於多少?答案不是0.1——是0。IEEE 754 浮點數在處理大數和小數同時存在的加減時,精度會丟失。Math.sumPrecise()就是為了解決這個問題。
開場:IEEE 754 的精度噩夢
// 這個例子每個 JavaScript 開發者都應該知道
console.log(0.1 + 0.2); // 0.30000000000000004
// 大數和小數同時存在時,更奇怪的問題:
const arr = [1e20, 0.1, -1e20];
const result = arr.reduce((a, b) => a + b, 0);
console.log(result); // 0(錯誤!實際上應該是 0.1)
// 解釋:1e20 + 0.1 = 1e20(0.1 被忽略,因為相對精度不足)
// 1e20 + 0.1 - 1e20 = 0(錯誤!)
為什麼會這樣?IEEE 754 雙精度浮點數的有效位數只有 53 位。當 1e20(約 17 位有效數字)和 0.1(約 1 位有效數字)相加時,因為差距太大,0.1 實際上被捨入進了誤差。
Math.sumPrecise() 的解決方案
// Stage 3 提案:Math.sumPrecise
const arr = [1e20, 0.1, -1e20];
// 使用 reduce(錯誤)
console.log(arr.reduce((a, b) => a + b, 0)); // 0(錯)
// 使用 Math.sumPrecise(正確)
console.log(Math.sumPrecise(arr)); // 0.1(正確!)
API 設計
// 只接受 Iterable,不接受 variadic
Math.sumPrecise([1, 2, 3]); // 6
Math.sumPrecise([1.1, 2.2, 3.3]); // 6.6
// 空 Iterable 返回 0
Math.sumPrecise([]); // 0
// NaN 或 Infinity 傳播
Math.sumPrecise([1, NaN]); // NaN
Math.sumPrecise([1, Infinity]); // Infinity
為什麼只接受 Iterable?
// 錯誤示範
Math.sumPrecise(1, 2, 3); // TypeError
// 過去的做法(缺點:arguments 轉陣列有開銷)
function sum(...args) { return Math.sumPrecise(args); }
// 好處:可以處理任何大小的資料集,不需要先把所有數放到棧上
這個設計決策讓 Math.sumPrecise 可以處理非常大的陣列(數百萬筆)而不會爆棧。
空 Iterable 返回 -0
Math.sumPrecise([]); // 返回 -0(並非 0)
// 這是數學上的一致性:空集合的加法單位元是 0
// 而在 JavaScript 中,-0 和 0 在大多數情況下是相等的
console.log(Math.sumPrecise([]) === 0); // true
演算法:Shewchuk 1996
Math.sumPrecise 使用的是 Shewchuk 1996 提出的自適應精度加法演算法:
// 演算法的核心思想:
// 不斷追蹤計算過程中的「誤差」,並在最後加回去
// 概念上的示意(實際實作可能不同)
function sumPrecise(numbers) {
let sum = 0;
let compensation = 0; // 追蹤誤差
for (const x of numbers) {
const adjusted = x + compensation;
const newSum = sum + adjusted;
// 補償:這次加法的誤差
compensation = adjusted - (newSum - sum);
sum = newSum;
}
return sum + compensation; // 最後加回補償
}
實際應用場景
金融計算:訂單金額加總
// 過去(錯誤)
const amounts = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0];
const total = amounts.reduce((a, b) => a + b, 0);
console.log(total); // 可能是 4.999999999999999(錯!)
// Math.sumPrecise(正確)
const total = Math.sumPrecise(amounts);
console.log(total); // 5.5(正確!)
科學計算:感測器資料聚合
// 感測器資料可能從極小到極大
const sensorData = [0.000001, 1000000, -999999.999999, 0.999998];
// reduce 會嚴重丟失精度
const wrongSum = sensorData.reduce((a, b) => a + b, 0);
// 可能是 1.000001(嚴重錯誤)
// Math.sumPrecise 給出正確答案
const correctSum = Math.sumPrecise(sensorData);
console.log(correctSum); // 1.000001
目前狀態:Stage 3
Math.sumPrecise 目前是 Stage 3,還沒有瀏覽器原生支援,但可以使用 proposal-math-sum 的 reference implementation 作為 polyfill:
// 作為 polyfill
import 'math-sum-precise';
console.log(Math.sumPrecise([1e20, 0.1, -1e20])); // 0.1
總結
Math.sumPrecise 解決了一個所有 JavaScript 開發者都踩過的坑——浮點數加法的精度問題:
// 以後金融計算
const balance = Math.sumPrecise(transactions.map(t => t.amount));
// 大資料集科學計算
const total = Math.sumPrecise(sensorReadings);
這是 TC39 新語法深度解析系列的第十一篇。TC39 新語法深度解析系列到此結束。
延伸閱讀
- 前端數字精度完整攻略 — JavaScript 浮點數運算與精度問題詳解
本文基於 TC39 Math.sumPrecise 提案(Stage 3)整理。