资讯动态

为什么 parseInt 转换大数字会丢精度?我用 BigInt 重写了一个进制转换器

发布时间:2026/10/14 15:28:22 来源:尧图企业网站定制
为什么 parseInt 转换大数字会丢精度我用 BigInt 重写了一个进制转换器做进制转换工具时发现一个经典陷阱超过Number.MAX_SAFE_INTEGER2^53的数字parseInt和Number都会丢精度。本文聊聊为什么会丢、怎么用 BigInt 手写一个不丢精度的进制转换器。工具体验tools.memory.gd.cn/encoding/radix先复现这个 bug你以为下面这段代码能正确转换parseInt(1111111111111111111111111111111111111111,2).toString(16)// 期待FFFFFFFFFFFFFFF// 实际1000000000000000精度丢了或者更直观Number(9007199254740993)// 2^53 1// 9007199254740992 ← 尾巴变了2^53 1 变成了 2^53这不是 bug是 IEEE 754 双精度浮点数的本质限制。为什么会丢JS 的 Number 是浮点数JavaScript 里没有真正的「整数」类型BigInt 出现前Number是 64 位浮点数。双精度浮点数的有效位是 52 位外加 1 位符号、11 位指数能精确表示的整数范围是-2^53到2^53也就是Number.MAX_SAFE_INTEGER 9007199254740991超过这个数相邻两个整数就无法区分了——2^53和2^53 1在内存里是同一个表示。所以一个 40 多位的二进制数parseInt转成 Number 时已经丢精度再.toString(16)出来的十六进制自然也是错的。这个坑在「进制转换」工具里特别明显因为用户就是要转大数——身份证号、雪花 ID、大哈希值随随便便就超 2^53。解法BigInt 逐字符解析BigInt是 ES2020 引入的任意精度整数能表示任意大的整数不丢精度。但BigInt的构造函数只认十进制字符串字面量不能直接BigInt(ff, 16)BigInt(ff)// SyntaxError不是 255BigInt(0xff)// 255n ✓ 要带前缀而且BigInt也没有toString(radix)之外的转换 API只有 2/8/10/16。非标准进制比如 36 进制得自己写。所以我手写了一个逐字符累加的解析函数functionparseBigInt(str:string,radix:number):bigint{letresultBigInt(0);constbaseBigInt(radix);for(constchofstr.toLowerCase()){letdigit:number;if(ch0ch9)digitch.charCodeAt(0)-48;elseif(chachz)digitch.charCodeAt(0)-87;elsethrownewError(非法字符${ch});if(digitradix)thrownewError(字符${ch}不属于${radix}进制);resultresult*baseBigInt(digit);}returnresult;}原理就是小学学的「按权展开」一个 N 进制数dₙdₙ₋₁...d₁d₀的值是dₙ × Nⁿ dₙ₋₁ × Nⁿ⁻¹ ... d₁ × N d₀从高位到低位每读一位就把已累加的结果乘以 N 再加当前位。全过程用 BigInt 运算不会溢出。字符到数字的映射0-9→ 0-9charCode 48-57减 48a-z→ 10-35charCode 97-122减 87所以这套代码其实支持到 36 进制只是工具 UI 只开放二/八/十/十六这 4 个常用项。转换出来的主函数解析回 BigInt 后输出端可以直接用big.toString(radix)exportfunctionconvertRadix(input:string,from:RadixType):ResultRadixResult{consttrimmedinput.trim().replace(/^0[xXbBoO]/,);// 去 0x/0b 前缀if(!trimmed)return{ok:false,error:请输入数值};constnegativetrimmed.startsWith(-);// 处理负号constdigitsnegative?trimmed.slice(1):trimmed;if(!digits)return{ok:false,error:无效的${from}数值};try{constradixRADIX_MAP[from];// { BIN:2, OCT:8, DEC:10, HEX:16 }constbigparseBigInt(digits,radix);constsignnegative?-:;return{ok:true,value:{BIN:signbig.toString(2),OCT:signbig.toString(8),DEC:signbig.toString(10),HEX:signbig.toString(16).toUpperCase(),},};}catch(e){return{ok:false,error:(easError).message};}}几个细节去前缀用户可能粘0xFF或0b1010用正则^0[xXbBoO]去掉再解析负号单独处理-FF这种把-拿出来解析完再加回去。不能把-当成字符塞进parseBigInt会抛「非法字符」toString(16)后大写十六进制习惯大写加.toUpperCase()错误信息具体哪个字符不属于哪进制直接告诉用户而不是笼统的「格式错误」坑上加坑TypeScript 编译报错「BigInt literals not available」写完一跑next build报这个错BigInt literals are not available when targeting lower than ES2020定位到这一行letresult0n;// ← 这里原因项目的tsconfig.json的target设的是低于 ES2020 的版本为了兼容老浏览器而0n这种 BigInt 字面量是 ES2020 才有的语法编译器不让用。解法是用BigInt()函数构造而不是字面量letresultBigInt(0);// ✓ 运行时和 0n 等价但不依赖 ES2020 targetconstbaseBigInt(radix);同样result * base BigInt(digit)里的BigInt(digit)也是这个原因——把普通 number 显式转成 bigint 才能参与运算。这是一个「同一门语言、同一份代码、不同编译目标下行为不同」的典型例子。BigInt是运行时就支持的能力现代浏览器都有但字面量0n是编译器层面的语法约束两个不是一个维度。效果对比拿一个 40 位的二进制数对比1111111111111111111111111111111111111111改之前parseInt NumberBIN: 1111111111111111111111111111111111111111 DEC: 549755813888错的应该是 1099511627775 HEX: 7ffffffff错的位数都不对改之后BigIntBIN: 1111111111111111111111111111111111111111 OCT: 17777777777 DEC: 1099511627775 HEX: FFFFF精准大数也不丢位。雪花 ID、大哈希、位运算掩码这种场景终于能用了。什么时候不需要 BigInt不是所有进制转换都要上 BigInt。如果你的输入保证小于 2^53比如普通的颜色值#FF0000、权限掩码、UUID 片段parseInt又快又简单parseInt(ff0000,16)// 16711680足够不需要 BigIntBigInt 的代价是运算比 Number 慢、不能和 Number 直接混用1n 1报错、序列化要手动.toString()。所以原则是确认会超 2^53 才用 BigInt否则 parseInt 就够。我做的这个工具面向的是「任意大数」场景用户不知道上限所以直接 BigInt 保底省得在边界判断上再留坑。小结进制转换看似简单三个点容易翻车Number 浮点精度——2^53 是硬上限超过就静默丢位BigInt 不能直接认非十进制字符串——要手写按权展开或带0x前缀TS target ES2020 不能用0n字面量——改用BigInt(0)构造这三点任何一个没处理好用户转个大数就会拿到错误结果而且没有任何报错——这是最危险的因为用户会信以为真。工具体验tools.memory.gd.cn/encoding/radix二/八/十/十六互转大数不丢精度。更多开发者工具 tools.memory.gd.cn19 个工具全部免费无需登录。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑