告别低精度累加:JavaScript Math.sumPrecise() 完全解析

归类于 javascript » 🏹系统分享 👋分享到微博 当前评论 0 条。

深入解析 JavaScript Math.sumPrecise() 方法的工作原理、语法规则与浏览器兼容性,通过“大数吃小数”实例对比 reduce、for 循环和 Kahan 补偿求和的结果,说明它如何避免中间累加精度损失并正确舍入最终结果,同时介绍 Number 类型限制、空集合返回 -0、TypeError 异常及旧浏览器 Polyfill 等注意事项。

如果你在 JavaScript 里做过求和运算,大概率遇到过这种情况:

0.1 + 0.2 === 0.3; // false,实际结果是 0.30000000000000004

这是 IEEE 754 双精度浮点数带来的经典精度问题。当你把一组数字放进循环或 reduce 里累加时,中间结果会不断舍入,最终误差可能逐渐累积。Math.sumPrecise() 正是为减少这类中间累加误差而设计的新方法。截至目前它已经进入 ECMAScript 2027 规范草案,新版本 Chrome、Firefox 和 Safari 等浏览器已开始原生支持,但旧版本浏览器仍可能无法使用。本文将带你弄清楚它解决了什么、怎么用,并给出可以直接运行的 Demo。

Math.sumPrecise() - DeathGhost
Math.sumPrecise()

为什么普通的累加会“算不准”?

先看一个经典的“大数吃小数”场景:

const numbers = [1e20, 1, -1e20];
const normalSum = numbers.reduce((acc, val) => acc + val, 0);
console.log(normalSum); // 0,数学上的结果应为 1

为什么会是 0?

1e201 时,由于双精度浮点数只能保持约 15~17 位有效数字,1 相对于 1e20 太小,在这一步被舍入丢失。因此,1e20 + 1 的浮点运算结果仍然是 1e20。然后再减去 1e20,最终得到 0。数学上的正确答案是 1,但累加过程中的中间部分和损失了精度。

Array.prototype.reduce 和普通 for 循环相加,每一步都会把中间结果保存为一个 IEEE 754 双精度浮点数,已经丢失的信息无法在后续步骤中恢复。这就是 Math.sumPrecise() 要解决的核心问题。

Math.sumPrecise():减少中间累加误差

Math.sumPrecise() 是一个静态方法,接收一个由 Number 值组成的可迭代对象(例如数组、TypedArray、Set 等),返回这些 Number 值的和。它返回的结果相当于先对输入的浮点数值进行精确数学求和,再将最终结果正确舍入为最接近的双精度浮点数。

基本语法

const sum = Math.sumPrecise(iterable);
Math.sumPrecise(["1", "2"]); // TypeError
Math.sumPrecise([1n, 2n]);   // TypeError

const emptySum = Math.sumPrecise([]);
console.log(Object.is(emptySum, -0)); // true

再来看刚才那个大数与小数混合的例子:

const numbers = [1e20, 1, -1e20];

const preciseSum = Math.sumPrecise(numbers);
console.log(preciseSum); // 1

ECMAScript 规范规定了 Math.sumPrecise() 必须产生的结果,但不会要求所有 JavaScript 引擎采用完全相同的内部算法。实现可以使用精确累加器、展开算法或其他能够满足规范正确舍入要求的技术。

传统求和方法 vs Math.sumPrecise()

下面给出一个可以直接在浏览器控制台或新建 HTML 文件中运行的示例。它会:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Math.sumPrecise Demo</title>
</head>
<body>
  <h2>浮点数累加对比</h2>
  <pre id="output"></pre>

  <script>
    // 构造测试数组:经典“大数吃小数” + 一组小数
    const numbers = [
      1e20, 1, -1e20,
      0.1, 0.2, 0.3, 0.4,
      0.5, 0.6, 0.7, 0.8, 0.9, 1.0
    ];

    // 方法1:Array.reduce 朴素求和
    const reduceSum = numbers.reduce((acc, val) => acc + val, 0);

    // 方法2:for 循环
    let loopSum = 0;
    for (let i = 0; i < numbers.length; i++) {
      loopSum += numbers[i];
    }

    // 方法3:Kahan 补偿求和
    // 注意:这只是对照算法,不是 Math.sumPrecise 的规范 Polyfill。
    function kahanSum(iterable) {
      let sum = 0;
      let compensation = 0;

      for (const num of iterable) {
        if (typeof num !== "number") {
          throw new TypeError("kahanSum only accepts Number values");
        }

        const y = num - compensation;
        const t = sum + y;
        compensation = (t - sum) - y;
        sum = t;
      }

      return sum;
    }

    const compensatedSum = kahanSum(numbers);

    // 方法4:原生 Math.sumPrecise(若可用)
    const nativeAvailable = typeof Math.sumPrecise === "function";
    const nativeSum = nativeAvailable
      ? Math.sumPrecise(numbers)
      : null;

    const output = document.getElementById("output");
    let result = `数组:[${numbers.join(", ")}]\n\n`;
    result += "输入 Number 值的精确数学和: 6.5\n\n";
    result += `reduce 求和             : ${reduceSum}\n`;
    result += `for 循环求和            : ${loopSum}\n`;
    result += `Kahan 补偿求和          : ${compensatedSum}\n`;

    if (nativeAvailable) {
      result += `Math.sumPrecise 原生    : ${nativeSum}\n\n`;
      result += "✅ 当前浏览器支持 Math.sumPrecise,本例返回 6.5。";
    } else {
      result += "Math.sumPrecise 原生    : 尚未支持\n\n";
      result +=
        `⚠️ 当前浏览器不支持 Math.sumPrecise。` +
        `本例中的 Kahan 结果为 ${compensatedSum};` +
        "它可能改善部分累加误差,但不保证与 Math.sumPrecise 等价。";
    }

    output.textContent = result;
  </script>
</body>
</html>

在支持 Math.sumPrecise() 的浏览器中,你应该会看到类似下面的输出:

数组:[100000000000000000000, 1, -100000000000000000000, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1]

输入 Number 值的精确数学和: 6.5

reduce 求和             : 5.5
for 循环求和            : 5.5
Kahan 补偿求和          : 5.5
Math.sumPrecise 原生    : 6.5

reducefor 循环在累加时丢失了夹在两个大数之间的 1,所以最终得到 5.5

普通的 Kahan 补偿求和可以改善很多常见的累加误差,但在这组特定顺序的数据中同样得到 5.5,没能保住夹在 1e20-1e20 之间的 1。因此,不能将这个简单的 Kahan 实现当作 Math.sumPrecise() 的等价 Polyfill。

Math.sumPrecise() 按照规范对输入 Number 值进行精确累加并正确舍入,因此本例返回 6.5

See the Pen Math.sumPrecise Demo by 孙志锋 (@deathghost) on CodePen.

不只是数组求和

Math.sumPrecise() 可以接收不同类型的可迭代对象,例如 SetMapvalues() 和生成器;前提是迭代产生的每个元素都是 Number 值:

const set = new Set([0.1, 0.2, 0.3, 0.2]); // Set 自动去重
console.log(Math.sumPrecise(set));          // 0.6

function* values() {
  yield* [1e-10, 2e10, -2e10, 1e-10];
}
console.log(Math.sumPrecise(values()));     // 2e-10

它也可以直接处理 Float64Array 等类型化数组,不必手动编写求和循环。

Math.sumPrecise() 不能解决所有小数精度问题

Math.sumPrecise() 避免的是求和过程中产生的中间舍入误差,但不会改变输入 Number 值本身的 IEEE 754 表示。例如:

console.log(Math.sumPrecise([0.1, 0.2]));
// 0.30000000000000004

这是因为 0.10.2 在进入方法之前就已经是最接近相应十进制数的二进制浮点近似值。Math.sumPrecise() 求出的是这些实际 Number 值的正确舍入和,而不是任意精度的十进制结果。因此,在货币等必须严格按照十进制规则计算的场景中,仍应使用整数最小单位、十进制运算库或专门的 Decimal 类型方案。

最后

在需要可靠浮点求和、且运行环境支持该方法时,Math.sumPrecise() 是比普通 reduce 或顺序循环更稳妥的选择。不过它解决的是累加过程中的中间舍入问题,并不会让 JavaScript 的 Number 变成任意精度十进制类型。你可以打开浏览器控制台运行上面的 Demo,看看当前浏览器是否已经支持这一特性。

这篇文章由 DeathGhost 编辑,发布于


Math.sumPrecise JavaScript 浮点数精度 累加误差 精确求和 Kahan补偿求和 ECMAScript 2027 TC39 大数吃小数 reduce求和

上一篇: