JavaScript 中为什么会有 BigInt 的提案?
JavaScript 为什么需要 BigInt一、根本原因Number 类型无法精确表示大整数JavaScript 从诞生之初就只有一种数字类型——IEEE 754 双精度浮点数64 位即Number。它能精确表示的整数范围是-(2^53 - 1) ~ (2^53 - 1) 即 -9,007,199,254,740,991 ~ 9,007,199,254,740,991这个值由Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER定义Number.MAX_SAFE_INTEGER;// 9007199254740991Number.MIN_SAFE_INTEGER;// -9007199254740991超过这个范围后Number 不再精确——整数会跳着走。二、Number 不精确的表现2.1 超出安全整数范围后精度丢失// 安全范围内精确90071992547409919007199254740991;// trueNumber.MAX_SAFE_INTEGER1;// 9007199254740992 ✓// 超出安全范围开始丢精度90071992547409929007199254740993;// true ← 两个不同的数被判定相等9007199254740993;// 9007199254740992 ← 返回的不是你输入的值9007199254740994;// 9007199254740994 ✓ 偶数精确见下文// 更大的数Number.MAX_SAFE_INTEGER10;// 9007199254741000 ← 实际结果跳过了 9007199254741001~9007199254741003原因IEEE 754 双精度浮点数只有 53 位有效数字1 位符号 11 位指数 52 位尾数隐含 1 位整数位超过 53 位的整数无法全部表示尾部被截断。IEEE 754 双精度结构 ┌──────┬──────────┬───────────────────────────┐ │ 符号 │ 指数 │ 尾数52位 │ │ 1位 │ 11位 │ 隐含的1位 53位有效精度 │ └──────┴──────────┴───────────────────────────┘ 53位能表示的最大精确整数 2^53 - 1 9,007,199,254,740,991 超过后每增加 2尾数 LSB 被丢弃一位2.2 大数运算错误// 64 位整数 ID数据库常见场景// Twitter 雪花算法生成的 ID1330000000000000000letid1330000000000000000;id1;// 1330000000000000000 ← 加 1 无效idid1;// true ← 两个值相等// 财务计算需要大整数表示分/厘// 1 亿元 100,000,000 元 10,000,000,000 分// 完全在 Number 安全范围内但如果用厘或更大单位// 1 万亿元 10,000,000,000,000,000 厘 → 精度丢失2.3 奇偶性判断失效9007199254740993%2;// 0 ← 应该是 1奇数却返回 0偶数// 因为 9007199254740993 在存储时已被截断为 90071992547409929007199254740992%2;// 0 ← 偶数正确// 偶数精确的原因偶数的二进制末位是 0截断不影响// 奇数的二进制末位是 1截断后变成偶数2.4 无法精确表示的常见数值0.10.2;// 0.30000000000000004 ← 浮点精度问题不仅是大数问题0.10.20.3;// false// 这是浮点数本身的局限BigInt 解决的是整数侧的问题// 小数精度问题需要用 decimal.js 等库不是 BigInt 的目标三、问题从何而来3.1 历史原因JavaScript 诞生于 1995 年Brendan Eich10 天开发。当时的设计目标决策原因只有一种数字类型简化语言降低学习门槛使用 IEEE 754 双精度与 Java 保持一致“Java 的简化版”不设计整数类型当时 Web 场景不需要大整数1995 年的 Web表单验证、简单动画、DOM 操作——数字运算需求极低这个设计完全够用。3.2 时代变了25 年后的 JavaScript 使用场景场景需求Number 能否满足数据库 64 位整数 ID精确表示和传递不能密码学 / 加密运算大整数模运算、素数生成不能高精度时间戳微秒/纳秒级时间戳不能金融计算大额整数精确运算不能社交平台数据计数点赞数、消息数超过 2^53不能社交媒体 IDTwitter/X、Instagram 的 64 位 ID不能随着 JS 从浏览器脚本变为全栈语言和大数据处理语言Number 的 53 位精度成为硬伤。四、已有的解决方案及其缺陷在 BigInt 出现之前开发者用什么方法每种都有严重问题4.1 字符串存储// 用字符串传递大整数letbigId1330000000000000000;bigId1;// 13300000000000000001 ← 字符串拼接不是数学加法bigId-1;// NaN ← 字符串不能减法bigId999;// false ← 字符串逐字符比较优点缺点不丢精度不能做数学运算JSON 友好比较语义错误字典序 vs 数值序简单每次运算都要自己实现逻辑4.2 第三方库bignumber.js / big.js / decimal.jsconstBigrequire(big.js);letanewBig(1330000000000000000);letbnewBig(999999999999999999);a.plus(b);// 2329999999999999999 ✓a.minus(b);// 330000000000000001 ✓a.eq(b);// false ✓优点缺点功能完整额外依赖精确运算不同库 API 不兼容支持小数与原生运算符无法互通不能写a b社区成熟与 JSON 序列化不兼容最致命的问题// 无法使用原生运算符ab;// [object Object]999999999999999999 ← 不是你想要的a.plus(b);// 必须调用方法// 无法用 / 比较ab;// false ← 对象引用比较非值比较a.eq(b);// 必须调用方法// JSON 序列化问题JSON.stringify({id:newBig(1330000000000000000)});// {id:{}} ← 大数信息丢失4.3 拆分为高低 32 位// 手动模拟 64 位整数functionint64Add(high1,low1,high2,low2){letlowlow1low2;letcarrylow0xFFFFFFFF?1:0;lowlow0xFFFFFFFF;lethighhigh1high2carry;return{high:high0,low:low0};}优点缺点无外部依赖实现极其复杂原生性能只能处理固定位宽如 64 位代码可读性极差容易出 Bug4.4 现有方案的共同问题问题说明无法用运算符a b不是大数加法无法用比较符a b不是数值比较互操作性差库 A 的大数不能直接和库 B 的大数运算JSON 不兼容序列化/反序列化丢失信息类型系统不统一语言层面没有大整数这个类型这些问题的根源JavaScript 语言本身没有原生大整数类型。无论库做得多好都不可能从根本上解决无法与原生运算符互通这个问题——这需要语言层面的支持。五、BigInt 提案的目标TC39ECMAScript 标准委员会在 2016 年提出 BigInt 提案Stage 0 → Stage 4 → ES2020 正式纳入核心目标目标说明语言级原生类型不是库是语言本身的一部分任意精度不限于 64 位理论上无限大受内存限制支持原生运算符可以写a b、a b、a b不破坏现有语义不改变 Number 的行为不引入兼容性问题六、BigInt 的设计决策与取舍6.1 为什么是新类型而非扩展 Number方案 A扩展 Number 精度未采纳将 Number 从 64 位浮点数改为 80 位或 128 位问题说明破坏兼容性所有现有代码的行为可能改变浮点精度、位运算等性能下降更大精度 更慢运算而 99% 的场景不需要大整数永远不够128 位也有上限问题只是推迟了不是整数扩展浮点精度不等于整数精确0.10.2 仍然不精确方案 B新增 BigInt 类型采纳新增一种原始类型 bigint与 number 并存优势说明不破坏兼容性Number 行为完全不变按需使用需要大整数时用 BigInt不需要时用 Number任意精度不限固定位宽理论上无限大真正的整数没有小数部分没有浮点精度问题// Number 和 BigInt 并存互不影响letn42;// number53位精度letb42n;// bigint任意精度typeofn;// numbertypeofb;// bigintnb;// false ← 不同类型 不转换nb;// true ← 会宽松转换6.2 为什么不允许与 Number 混合运算1n2;// TypeError: Cannot mix BigInt and other types2n1;// TypeErrorMath.max(1n,2);// TypeError设计理由担忧说明精度丢失方向不明确BigInt Number返回什么类型转 BigInt 可能丢浮点信息转 Number 可能丢整数精度隐式转换行为不可预测开发者不知道结果是什么类型强制显式转换更安全让开发者明确知道转换在哪里发生必须显式转换// BigInt → Number可能丢失精度Number(100000000000000000000n);// 1e20 ← 精度丢失// Number → BigInt截断小数部分BigInt(3.14);// 3n ← 截断不四舍五入BigInt(-0);// 0n ← -0 和 0 都是 0nBigInt(NaN);// TypeErrorBigInt(Infinity);// TypeError// 安全转换 Number → BigIntfunctionsafeToBigInt(n){if(!Number.isFinite(n))thrownewTypeError(无法转换);if(!Number.isSafeInteger(n))thrownewRangeError(超出安全范围);returnBigInt(n);}6.3 为什么不能 new BigInt()BigInt(42);// 42n ← 函数调用 ✓newBigInt(42);// TypeError: BigInt is not a constructor ✗设计理由避免与包装类型相关的问题new Boolean(false)是 truthy 等ES6 的Symbol和 ES2020 的BigInt采用了同样的设计——禁止new强制使用原始值。6.4 为什么 BigInt 不能用 Math 方法Math.max(1n,2n);// TypeErrorMath.floor(3.14n);// TypeErrorBigInt 本身就是整数无需 floor设计理由Math方法的参数类型定义为numberBigInt 不兼容BigInt 不需要floor/ceil/round等方法本身就是整数需要时用对应的运算符或方法// BigInt 的等价操作// Math.abs(x) → x 0n ? -x : x 或 (x ** 2n) ** 0.5n不精确不建议// Math.max(a, b) → a b ? a : b// Math.min(a, b) → a b ? a : b6.5 为什么不支持小数3.14n;// SyntaxError设计理由小数精度问题与整数精度问题是两个独立问题大数小数场景如财务需要的是固定精度十进制不是任意精度二进制这由BigDecimal提案负责目前仍在 Stage 1尚未纳入标准// 目前需要用库处理小数大数importDecimalfromdecimal.js;newDecimal(12345678901234567890.123456789);七、BigInt 的内部实现不同引擎的实现方式不同但核心思想一致——用多个数字段拼接表示一个整数简单模型以 32 位分段 42n 的内部表示 [42] ← 一个 32 位段就够 1330000000000000000n 的内部表示 [0x07B2DC90898A, 0x049] ← 两个 32 位段 任意大小的整数用更多段 [段0, 段1, 段2, ..., 段n] 高位 ──────────────────▶ 低位各引擎的实际实现引擎内部表示特点V8Chrome/Node符号 绝对值 动态长度的 30 位段数组每个 digit 占 30 bit避免乘法溢出 32 位SpiderMonkeyFirefox类似的动态分段与 V8 思路一致JavaScriptCoreSafari类似—V8 BigInt 内部结构简化 ┌──────────┬──────────────────┬─────────────────────────────┐ │ 符号位 │ 长度段数 │ digits[0..n-1]每段30位 │ │ 0正,1负 │ │ 低位在前 │ └──────────┴──────────────────┴─────────────────────────────┘ 示例 -42n → { sign: 1, length: 1, digits: [42] } 1330000000000000000n → { sign: 0, length: 2, digits: [0x2DC90898A, 0x49] }运算复杂度运算复杂度说明加/减O(n)线性遍历各段乘法O(n²)基础实现Karatsuba 可到 O(n^1.585)除法O(n²)长除法幂O(n² × log(exp))快速幂比较O(n)逐段比较其中 n 是 BigInt 的段数位数 / 30。对于常见的 64 位整数n2~3运算几乎与原生整数一样快。八、BigInt 解决的具体场景8.1 数据库 64 位整数 ID// 问题JSON.parse 后大数 ID 精度丢失JSON.parse({id: 1330000000000000000});// { id: 1330000000000000001 } ← 精度丢失// 解决方案后端返回字符串前端用 BigIntJSON.parse({id: 1330000000000000000});// { id: 1330000000000000000 }letuserIdBigInt(1330000000000000000);userId1n;// 1330000000000000001n ✓ 精确8.2 密码学运算// RSA 加密核心大素数的模幂运算constp61n;constq53n;constnp*q;// 3233nconstphi(p-1n)*(q-1n);// 3120n// 加密c m^e mod nconstm42n;conste17n;constcm**e%n;// 2557n// 实际 RSA 中 p、q 是数百位的大素数constbigPrime12n**1024n-1n;// 没有 BigInt这在 JS 中完全不可能8.3 高精度时间戳// 微秒级时间戳超出 Number 安全范围约 2255 年才会超出但某些系统用自定义纪元constusTimestampBigInt(Date.now())*1000nBigInt(performance.now()*1000);// 纳秒级更容易超出constnsTimestampBigInt(Date.now())*1000000n;8.4 大数计数// 社交平台点赞数、播放量等letviews0n;// 模拟一亿次播放for(leti0;i100000000;i){views1n;}views;// 100000000n ✓ 精确// 用 NumberletviewsNum0;// 1e20 1 1e20 → 精度丢失8.5 位运算// 64 位位运算Number 只能做 32 位位运算// Number 的位运算会先将操作数转为 32 位有符号整数0xFFFFFFFF;// 4294967295Number 正确表示0xFFFFFFFF0;// -1转为 32 位有符号整数后符号扩展// BigInt 的位运算保持全精度0xFFFFFFFFn;// 4294967295n ✓0xFFFFFFFFn0n;// 4294967295n ✓0xFFFFFFFFn32n;// 18446744069414584320n ✓64位结果0xFFFFFFFFFFFFFFFFn;// 18446744073709551615n2^64-1✓// 实际应用64位掩码操作constMASK_64BIT(1n64n)-1n;// 18446744073709551615nfunctiontoUint64(n){returnBigInt.asUintN(64,n);}toUint64(-1n);// 18446744073709551615n ✓toUint64(2n**63n);// 9223372036854775808n ✓九、BigInt 的限制与未解决的问题限制说明不能与 Number 混合运算必须显式转换不支持小数需要 BigDecimal 提案不能用 Math 方法需手动实现或用库JSON 不支持JSON.stringify(1n)→ 抛错性能不如 Number大位数运算比原生数字慢部分旧环境不支持IE 不支持Node 10.4 / Chrome 67JSON 的问题及解决方案// BigInt 默认不能 JSON 序列化JSON.stringify({id:1330000000000000000n});// TypeError: Do not know how to serialize a BigInt// 解决方案 1自定义 toJSONBigInt.prototype.toJSONfunction(){returnthis.toString();};JSON.stringify({id:1330000000000000000n});// {id:1330000000000000000}// 解决方案 2自定义序列化器JSON.stringify({id:1330000000000000000n},(key,value)typeofvaluebigint?value.toString():value);// 解决方案 3使用 json-bigint 库十、时间线时间事件1995JavaScript 诞生只有 NumberIEEE 754 双精度2009ES5仍未引入整数类型2010~2015Node.js 崛起JS 进服务端大整数问题暴露2016BigInt 提案进入 Stage 02017进入 Stage 2草案2018进入 Stage 3候选2019进入 Stage 4完成2020ES2020 正式纳入标准2020~至今Chrome 67、Firefox 68、Node 10.4、Safari 14 全部支持十一、总结BigInt 提案的根本原因是 JavaScript 从 1995 年设计至今唯一的数字类型 NumberIEEE 754 双精度浮点数只能精确表示 53 位整数而现代 JS 应用数据库 ID、密码学、金融、社交媒体越来越频繁地遇到 64 位甚至更大的整数运算需求。在 BigInt 之前开发者只能用字符串存储不能运算、第三方库不能原生运算符、手动拆位极其复杂等方式绕过每种方案都有致命缺陷。BigInt 作为语言级原生类型从根本上解决了这个问题任意精度、支持原生运算符、与 Number 并存互不干扰。代价是不允许与 Number 隐式混合运算、不支持小数、JSON 不原生支持——这些都是为不破坏现有语义和保持设计一致性所做的有意识的取舍。