怎样用JavaScript实现大数运算?

来源:IPIPP.com作者:头衔:全栈工程师
导读:本期聚焦于创作的《怎样用JavaScript实现大数运算?》,敬请观看详情。在JavaScript中,由于Number类型存在精度限制,最大安全整数为2的53次方减1,超过这个范围的数值运算会出现精度丢失问题。很多需要处理大额数值的场景,比如金融计算、密码学运算、大整数阶乘计算等,都需要我们手动实现大数运算逻辑。本文将介绍基于字符串处理的大数运算实现思路,覆盖大数加法、减法、乘法、除法的核心逻辑,同时给出可直接运行的代码示例,帮助开发者理解大数运算的底层原理,能够根据实际需求调整实现方案,解决JavaScript原生数值类型的精度不足问题。

JavaScript处理普通整数十分方便,但一旦数值超过安全整数范围,普通Number类型就无法保证每一位都精确。所谓大数运算,通常是指把整数当作字符串来保存,并模拟人工列竖式的过程,从低位到高位逐步完成加、减、乘、除。这样既能绕开二进制浮点数的精度限制,也能在面试、教学或特定业务场景中深入理解数值计算的基本原理。

怎样用JavaScript实现大数运算?

从Number精度边界理解大数运算

JavaScript的Number类型基于IEEE 754双精度浮点数表示,所有数字在底层都以有限位数的二进制形式存储。对于日常开发中的大多数小数和整数,这种表示足够方便,但它并不是无限精确的。整数能够被安全表示的范围是有限的,一旦超过这个范围,相邻整数之间可能出现无法区分的情况,最典型的表现就是加法、比较或序列化结果与预期不一致。

例如,当一个很大的整数再加1时,结果可能并不会精确地增加1,而是被舍入到附近可表示的浮点值。这类问题在金融计算、订单号处理、密码学参数、统计ID或超长数值展示中尤其敏感。业务代码也许并不需要进行复杂数学推导,但只要数值长度超过安全范围,就不能继续依赖普通Number直接运算。

因此,字符串模拟成为理解大数运算的经典方案。它把数字看成若干字符组成的序列,从最低位开始逐位计算,需要进位时记录进位,需要借位时记录借位。这个思路与手工竖式完全一致,不依赖语言内部的数值精度,也便于逐步扩展符号、小数和高性能算法。可以说,手写大数运算的价值不仅在于得到一个结果,更在于帮助开发者建立对数值边界和计算过程的直观认识。

用字符串模拟实现加法、减法、乘法和除法

下面的示例以非负整数字符串为基础,封装了一组可运行的工具函数。normalizeNumber用于去除前导零,compareAbs用于比较两个非负数字字符串的大小,addAbssubtractAbs分别处理绝对值意义上的加法和减法,multiplyAbsdivideAbs则实现乘法和整数除法。之所以先处理绝对值,是因为四则运算的复杂度主要来自逐位计算本身,把符号逻辑拆到外层后,核心算法会更清晰。

在加法中,两个指针从字符串末尾向前移动,每次取出当前数字相加,并把个位写入结果、把十位作为进位。减法的关键是先判断被减数是否足够大,如果不够大,就交换两个数并给最终结果补上负号;逐位相减时,如果当前位不够减,就向高位借1,本位加10后再继续。乘法则利用结果数组的固定长度,把每一位相乘的积累加到对应位置,最后统一处理前导零。除法采用长除法的思路,每次把余数拼接下一位被除数,再试出当前位能够放置的最大数字。

这段代码虽然以教学演示为主,但已经覆盖了大数运算中最常见的几个关键点:低位优先、进位借位、结果归一化、前导零清理,以及除法中的试商与余数更新。阅读时可以重点关注每个函数如何把复杂问题拆成单步操作,这也是后续做性能优化和符号扩展的基础。

function normalizeNumber(value) {
    var str = String(value).replace(/^0+/, '');
    return str === '' ? '0' : str;
}

function compareAbs(a, b) {
    a = normalizeNumber(a);
    b = normalizeNumber(b);
    if (a.length !== b.length) {
        return a.length > b.length ? 1 : -1;
    }
    if (a === b) {
        return 0;
    }
    return a > b ? 1 : -1;
}

function addAbs(a, b) {
    a = normalizeNumber(a);
    b = normalizeNumber(b);
    var i = a.length - 1;
    var j = b.length - 1;
    var carry = 0;
    var result = [];

    while (i >= 0 || j >= 0 || carry > 0) {
        var digitA = i >= 0 ? Number(a[i]) : 0;
        var digitB = j >= 0 ? Number(b[j]) : 0;
        var sum = digitA + digitB + carry;
        result.push(sum % 10);
        carry = Math.floor(sum / 10);
        i--;
        j--;
    }

    return normalizeNumber(result.reverse().join(''));
}

function subtractAbs(a, b) {
    // 调用前请确保 a 的绝对值大于等于 b
    a = normalizeNumber(a);
    b = normalizeNumber(b);
    var i = a.length - 1;
    var j = b.length - 1;
    var borrow = 0;
    var result = [];

    while (i >= 0 || j >= 0) {
        var digitA = i >= 0 ? Number(a[i]) : 0;
        var digitB = j >= 0 ? Number(b[j]) : 0;
        var diff = digitA - borrow - digitB;

        if (diff < 0) {
            diff += 10;
            borrow = 1;
        } else {
            borrow = 0;
        }

        result.push(diff);
        i--;
        j--;
    }

    return normalizeNumber(result.reverse().join(''));
}

function subtractSigned(a, b) {
    var cmp = compareAbs(a, b);
    if (cmp === 0) {
        return '0';
    }
    if (cmp < 0) {
        return '-' + subtractAbs(b, a);
    }
    return subtractAbs(a, b);
}

function multiplyAbs(a, b) {
    a = normalizeNumber(a);
    b = normalizeNumber(b);

    if (a === '0' || b === '0') {
        return '0';
    }

    var result = new Array(a.length + b.length).fill(0);

    for (var i = a.length - 1; i >= 0; i--) {
        var digitA = Number(a[i]);
        for (var j = b.length - 1; j >= 0; j--) {
            var pos = i + j + 1;
            var product = digitA * Number(b[j]) + result[pos];
            result[pos] = product % 10;
            result[pos - 1] += Math.floor(product / 10);
        }
    }

    return normalizeNumber(result.join(''));
}

function multiplyByDigit(a, digit) {
    a = normalizeNumber(a);

    if (a === '0' || digit === 0) {
        return '0';
    }

    var carry = 0;
    var result = [];

    for (var i = a.length - 1; i >= 0; i--) {
        var product = Number(a[i]) * digit + carry;
        result.push(product % 10);
        carry = Math.floor(product / 10);
    }

    if (carry > 0) {
        result.push(carry);
    }

    return normalizeNumber(result.reverse().join(''));
}

function divideAbs(dividend, divisor) {
    dividend = normalizeNumber(dividend);
    divisor = normalizeNumber(divisor);

    if (divisor === '0') {
        throw new Error('除数不能为0');
    }

    if (compareAbs(dividend, divisor) < 0) {
        return '0';
    }

    var quotient = '';
    var remainder = '0';

    for (var i = 0; i < dividend.length; i++) {
        if (remainder === '0') {
            remainder = dividend[i];
        } else {
            remainder = remainder + dividend[i];
        }

        remainder = normalizeNumber(remainder);

        var digit = 0;
        for (var d = 9; d >= 1; d--) {
            var product = multiplyByDigit(divisor, d);
            if (compareAbs(product, remainder) <= 0) {
                digit = d;
                break;
            }
        }

        quotient += String(digit);

        if (digit > 0) {
            remainder = subtractAbs(remainder, multiplyByDigit(divisor, digit));
        }
    }

    return normalizeNumber(quotient);
}

console.log(addAbs('9999999999999999', '1'));
console.log(subtractSigned('123456', '654321'));
console.log(multiplyAbs('123456', '654321'));
console.log(divideAbs('80779853376', '654321'));

工程实践中的优化方向与BigInt选择

手写字符串运算有助于理解原理,但在真实项目中要综合考虑性能、边界情况和维护成本。上面的实现按十进制逐位处理,直观但效率有限。当数字非常长时,可以改用万进制、亿进制等分块方式,把若干位十进制数放进一个数组元素中,从而减少循环次数和字符串拼接开销。乘法也可以从普通竖式升级为分治算法,除法可以通过更精确的试商策略减少重复比较。

  • 分块存储:用数组保存固定位数的数字块,而不是每个字符都参与循环。
  • 符号扩展:先解析正负号,再把同号相加、异号相减等规则拆分成独立逻辑。
  • 小数支持:先统一小数位数,将数字放大为整数计算,最后再恢复小数点位置。
  • 算法升级:在超长数字场景下,可以考虑更高效的乘法与除法策略,降低时间复杂度。

符号与小数也是必须面对的问题。符号处理通常先解析正负号,再根据同号、异号规则转换为绝对值加减;小数处理则需要先对齐小数点,把两个数统一放大成整数计算,最后再根据总小数位数恢复小数点位置。若业务涉及金额,除了大数问题,还要明确舍入规则、货币单位和展示格式,避免把数学问题与财务规则混在一起。

如今JavaScript已经提供了原生BigInt类型,适合处理任意精度整数。BigInt写法直观,加减乘除都可以直接使用,但它不能和普通Number混合运算,也不适合表示小数。对于需要兼容复杂场景、十进制精度控制或固定小数位运算的项目,也可以使用成熟第三方库,由库来处理边界情况与性能优化。

// BigInt适合整数运算,字面量后缀n即可
const a = 9999999999999999n;
const b = 1n;

console.log(a + b);
console.log(a * b);
console.log(typeof a);

// BigInt不能和普通Number直接混合运算
// 下面这行如果取消注释会抛出类型错误
// console.log(a + 1);

总结:先理解原理,再选择合适方案

回顾整个实现过程,大数运算并不神秘,它本质上是把人类熟悉的竖式计算转写成程序逻辑。加法关注进位,减法关注借位和大小比较,乘法关注位置累加,除法关注余数和试商。只要把每一步拆细,并处理好前导零、空位补零和结果归一化,就能得到稳定可控的字符串计算方案。

在实际开发中,如果只是偶尔展示超长整数,可以直接使用字符串或原生BigInt;如果需要频繁进行复杂计算,应优先选择经过验证的库,并针对性能瓶颈做分块与算法优化。理解手写实现的价值在于,当遇到精度异常、边界数值或自定义计算需求时,能够快速定位问题并选择合适的解决路径。

JavaScript大数运算字符串处理进制转换高精度计算修改时间:2026-08-15 14:27:10

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。