
搞懂世界上最大的数:从BigNumber源码看大数运算最佳实践
很多工程师在面试或实战中,一提到世界上最大的数就头大。你会写 1+1,但让你处理 1000 位精度的金融数据或密码学哈希,代码直接崩盘。这不是语法问题,是最佳实践缺失。
你卡在“学会语法却不知怎么搭项目”的瓶颈上。知道 Number.MAX_VALUE 是 \(1.8 \times 10^{308}\),但不知道如何在业务中安全地表示更大的数。今天拆解 bignumber.js 核心源码,看透大数运算的底层逻辑,解决你在大数处理上的工程难题。
入口定位:为什么原生 JS 不够用
在 JavaScript 中,所有数字默认是 IEEE 754 双精度浮点数。这意味着有效数字只有 53 位二进制,换算成十进制大约 15-17 位。一旦超过这个精度,后面的数字会被截断或变成近似值。
// 经典坑:浮点数精度丢失
console.log(0.1 + 0.2); // 0.30000000000000004// 超过 MAX_SAFE_INTEGER
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number.MAX_SAFE_INTEGER + 1); // 9007199254740992 (错误! 变成了偶数)当业务涉及世界上最大的数概念,比如区块链交易 ID、高精度科学计算,原生 Number 类型彻底失效。我们需要引入任意精度算术库。bignumber.js 是 Node.js 生态中轻量级且性能优异的解决方案。它的入口很简单,但核心实现却充满了工程智慧。
核心片段:BigNumber 构造函数与字符串解析
bignumber.js 的核心类是 BigNumber。它的构造函数接收一个数字、字符串或另一个 BigNumber 实例。关键在于,它如何解析输入并存储内部状态。
我们来看 src/bignumber.js 中的关键部分(简化版,保留核心逻辑):
// 语言: JavaScript
// 文件: bignumber.js 核心构造逻辑片段function BigNumber(v) {var x = this;var isBigNum = v instanceof BigNumber;// 1. 验证输入类型,防止非法值污染状态if (typeof v === 'number' v * 0 !== 0) {throw new TypeError('Invalid number: ' + v);}// 2. 如果是 BigNumber 实例,直接复制内部状态 (性能优化)if (isBigNum) {x.s = v.s; // 符号: 1 正, -1 负, 0 零x.e = v.e; // 指数: 科学计数法的指数部分x.c = v.c.slice(); // 系数: 数组形式存储的各位数字,避免引用共享return x;}// 3. 处理字符串输入,正则提取符号、系数、指数if (typeof v === 'string') {v = v.trim();var match = v.match(/^-?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i);if (!match) throw new TypeError('Invalid string: ' + v);x.s = v.charAt(0) === '-' ? -1 : 1;var numStr = v.replace(/^-?|e[+-]?\d+$/i, '');var expStr = v.match(/e([+-]?\d+)/i);// 解析系数:去掉小数点,计算指数偏移var dotIndex = numStr.indexOf('.');var coeffStr = numStr.replace('.', '');x.c = coeffStr.split('').map(Number);// 计算指数: 原始指数 + 小数点移动位数var exp = expStr ? parseInt(expStr[1], 10) : 0;if (dotIndex !== -1) {exp -= (coeffStr.length - (dotIndex + 1));}x.e = exp;// 4. 标准化:去除前导零和末尾零normalize(x);}
}逐行解读:类型安全:v * 0 !== 0 巧妙排除了 NaN 和 Infinity。
浅拷贝陷阱:v.c.slice() 至关重要。如果直接赋值 x.c = v.c,两个 BigNumber 实例会共享同一个数组,修改一个会导致另一个数据污染。这是源码中最容易踩的坑。
字符串解析:正则 /^-?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i 覆盖了整数、小数、科学计数法。
指数计算:exp -= (coeffStr.length - (dotIndex + 1)) 这行代码是核心。例如 123.45,系数是 12345,小数点后有 2 位,所以指数要减 2,变成 12345 * 10^-2。
标准化:normalize 函数确保内部存储的唯一性,比如 001.230 会变成 1.23。设计思想:数组存储与基数优化
为什么用数组 c 存储系数,而不是一个巨大的数字?因为 JS 的 Number 精度有限。用数组存储每一位数字,可以无限扩展精度。
但这里有个最佳实践:bignumber.js 默认使用 1e14 作为基数(Base),而不是 10。
// 语言: JavaScript
// 内部存储示例: 数字 12345678901234567890// 如果 Base = 10:
// c: [1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0]
// e: 19// 如果 Base = 1e14 (100000000000000):
// c: [12345678901234, 567890] // 每个元素最大不超过 99999999999999
// e: 19 - 13 = 6 (因为第一组占了14位,但这里需要仔细对齐)
// 实际逻辑: c[0] * BASE^1 + c[1] * BASE^0设计优势:减少数组长度:20 位的数字,Base=10 需要 20 个元素,Base=1e14 只需要 2 个。内存占用降低,循环遍历次数减少。
乘法效率:大数乘法是 O(n*m) 复杂度。元素越少,乘加操作次数越少。在 CSDN 的一篇深度解析文章中提到,对于 1000 位数字的乘法,Base=1e14 比 Base=10 快约 4 倍。
进位处理:虽然基数变大,进位时可能需要处理更大的中间值,但现代 CPU 处理 64 位整数非常快,JS 引擎对整数运算有优化,所以整体收益大于成本。手写简化版:加法与进位逻辑
理解设计思想后,我们手写一个极简版的大数加法,聚焦于进位逻辑。这是大数运算的灵魂。
// 语言: JavaScript
// 简化版 BigNumber 加法实现class SimpleBigNum {constructor(str) {// 简化:只处理正整数,不含小数点this.digits = str.split('').reverse().map(Number); // 低位在前this.sign = 1;}add(other) {let a = this.digits;let b = other.digits;let result = [];let carry = 0;let maxLen = Math.max(a.length, b.length);for (let i = 0; i maxLen; i++) {let sum = (a[i] || 0) + (b[i] || 0) + carry;// 核心:Base=10 的进位逻辑// 如果 sum = 10,当前位取 sum % 10,进位 1result.push(sum % 10);carry = Math.floor(sum / 10);}// 处理最终进位if (carry 0) {result.push(carry);}// 反转回高位在前,并转换为字符串return new SimpleBigNum(result.reverse().join(''));}toString() {return this.digits.reverse().join('');}
}// 测试
const num1 = new SimpleBigNum('99999999999999999999');
const num2 = new SimpleBigNum('1');
console.log(num1.add(num2).toString()); // 100000000000000000000避坑指南:低位在前:存储时反转数组,因为加法从最低位开始。如果高位在前,需要复杂的索引计算。
边界检查:a[i] || 0 处理长度不一致的情况。
最终进位:循环结束后,carry 可能不为 0,必须单独 push。
性能瓶颈:这个手写版只适用于 Base=10。实际工程中,必须使用 Base=1e14 或更高,否则处理 1000 位数时,数组操作开销巨大。应用场景:金融与区块链中的大数
金融领域:高精度计算是刚需。银行利息计算、股票交易价格,误差不能超过 0.0001。使用 bignumber.js 可以避免浮点数误差。
const BigNumber = require('bignumber.js');
const price = new BigNumber('123456789.123456789');
const quantity = new BigNumber('1000');
const total = price.times(quantity);
console.log(total.toString()); // 123456789123.456789区块链领域:以太坊使用 256 位整数。交易 Gas Fee 计算、地址哈希,都涉及超过 64 位的大数。bignumber.js 虽然轻量,但对于超高频交易场景,可能需要更专业的库如 bn.js,其内部使用 Base=2^26 优化位运算。
避坑:不要混合类型:始终用字符串初始化 BigNumber,避免 new BigNumber(0.1) 带来的精度丢失。
比较陷阱:a b 是 JS 原生比较,会转成浮点数。必须用 a.gt(b)。
序列化:JSON.stringify 不支持 BigNumber。自定义 toJSON 方法,返回字符串。结尾互动
大数运算不是玄学,是工程问题。世界上最大的数在计算机里只是一串数组,关键看你怎么存、怎么算。
你在项目中遇到过哪些大数处理的坑?是金融对账差一分,还是区块链 Gas 费算错?评论区留言,挨个回。