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

从Number精度边界理解大数运算
JavaScript的Number类型基于IEEE 754双精度浮点数表示,所有数字在底层都以有限位数的二进制形式存储。对于日常开发中的大多数小数和整数,这种表示足够方便,但它并不是无限精确的。整数能够被安全表示的范围是有限的,一旦超过这个范围,相邻整数之间可能出现无法区分的情况,最典型的表现就是加法、比较或序列化结果与预期不一致。
例如,当一个很大的整数再加1时,结果可能并不会精确地增加1,而是被舍入到附近可表示的浮点值。这类问题在金融计算、订单号处理、密码学参数、统计ID或超长数值展示中尤其敏感。业务代码也许并不需要进行复杂数学推导,但只要数值长度超过安全范围,就不能继续依赖普通Number直接运算。
因此,字符串模拟成为理解大数运算的经典方案。它把数字看成若干字符组成的序列,从最低位开始逐位计算,需要进位时记录进位,需要借位时记录借位。这个思路与手工竖式完全一致,不依赖语言内部的数值精度,也便于逐步扩展符号、小数和高性能算法。可以说,手写大数运算的价值不仅在于得到一个结果,更在于帮助开发者建立对数值边界和计算过程的直观认识。
用字符串模拟实现加法、减法、乘法和除法
下面的示例以非负整数字符串为基础,封装了一组可运行的工具函数。normalizeNumber用于去除前导零,compareAbs用于比较两个非负数字字符串的大小,addAbs和subtractAbs分别处理绝对值意义上的加法和减法,multiplyAbs和divideAbs则实现乘法和整数除法。之所以先处理绝对值,是因为四则运算的复杂度主要来自逐位计算本身,把符号逻辑拆到外层后,核心算法会更清晰。
在加法中,两个指针从字符串末尾向前移动,每次取出当前数字相加,并把个位写入结果、把十位作为进位。减法的关键是先判断被减数是否足够大,如果不够大,就交换两个数并给最终结果补上负号;逐位相减时,如果当前位不够减,就向高位借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