那句「0~1000 不会溢出」是怎么让我起疑的
这页文案写着「基于 BigInt,支持计算 0~1000 的阶乘,不会溢出」,同一句里还藏着另一半:Number 最大约 1.7×10^308(170!),更大才需要 BigInt。于是我用最普通的写法复核了 23 的阶乘——var r = 1; for (var i = 2; i <= 23; i++) r *= i; 得到 25852016738884978212864,真值却是 25852016738884976640000,差 1572864,第一个错位落在 23 位数字的第 17 位。23 就在页面宣称的区间之内,比「170 才出问题」早了 147 档。
结论先放着:页面本身不犯这个错,它没有 Number 的乘法路;会错的是我们在 Excel 或随手写的脚本里复核它的那一步。
模板里只有一条累乘路,第 111 行起就是 BigInt
factorialBig(第 108-116 行)只有一个循环:第 111 行 var result = 1n;,第 112 行 for (var i = 2n; i <= BigInt(n); i++),第 113 行 result *= i;。全文 BigInt 种子 1 处、Number 种子 0 处,连 5! 也是 BigInt 算的;文案那句「更大需用 BigInt」描述的是一次根本不存在的切换。
溢出档在 171,精度档在 23:两条分界不在同一处
| 口径 | 分水岭 | 那一档的实际值 |
|---|---|---|
| Number 精确性 | 23! | 比真值多出差额 1572864 |
| Number 有限性 | 171! | 170! 仍是 7.257415615307994e+306,171! 起为 Infinity |
| 页面 BigInt | 无分界 | 170! 给 307 位、171! 给 310 位、1000! 给 2568 位 |
还有个细节:170! 与 171! 的尾随零同为 41 个(171 不含因子 5),位数却从 307 涨到 310。「不溢出」与「数字对」是两件事,前者晚 147 档,后者在 22! 之后就失守。
图 1:横轴为 n,条长是该口径靠得住的区间。
1000 是输入闸门,不是算法上限
第 142 行是 if (n > 1000),用的「大于」,所以 1000 本身放行:1000! 裸值 2568 位,带千分位显示成 3423 个字符,多的 855 个逗号只是格式化,第 169 行的「结果位数」取的是裸值长度。输 1001 是硬报错,toast 原文「n 不能超过 1000」,不截断也不静默。把闸门绕过去直调函数,1001! 得 2571 位、2000! 得 5736 位——页面上输不进去,但足以说明 1000 只是闸门。
249 个尾随零:页面上没这个字段,是脚本口径
口径先说清:第 168、169、170 行的统计块只有 3 项,n、结果位数、是否超长结果,对模板 grep「尾随」「零个数」是 0 匹配。所以这数是自己数的——拿 BigInt 值末尾的 0 去撞勒让德公式 floor(n/5)+floor(n/25)+floor(n/125)+floor(n/625),n = 0..1000 全档 0 处不一致:1000! = 200+40+8+1 = 249 个,500! 是 124 个,100! 是 24 个,25! 是 6 个。
这条阶梯本身有洞:1~249 里只有 201 个值取得到,缺 49 个,最小的洞是 5——24! 还是 4 个零,25! 直接跳 6 个,因为 25 一次贡献两个 5,125 处一次贡献 3 个。
图 2:尾随零个数随 n 走的阶梯(脚本口径)。
最隐蔽的失败路:parseInt 在字母处停住
第 137 行是 parseInt(nStr, 10),它在第一个非数字字符处停住且不报错,于是写法上最大的数能算出最小的结果:
1e21读成 1,页面安静显示 1! = 1,第 142 行的闸门根本没触发。1,000与1e5同样得 1;0x10在 x 处停住得 0。- 小数向零截断:
5.5与5.9都读成 5,出 120,无提示。 - 空串、
abc、全角20落进第 140 行isNaN分支,只把结果区藏掉。 - 输
1001反倒最干净:明确一句「n 不能超过 1000」。
范围列表:文案说 100 项,代码放行 101 项
第 63 行提示写「列表最多展示 100 项」,第 194 行的护栏却是 if (n - m > 100)——比差值不比项数。于是 m=0, n=100 放行,第 213 行自己打出「项数:101」,列表 HTML 18133 字符;m=1, n=101 与 m=50, n=150 也是 101 项。反过来 m=0, n=1000 被拒,拒绝语「范围过大,最多支持 100 项」,所以把 0! 到 1000! 整列出来这个操作在页面上不存在。
耗时全是脚本口径(Node v24.19.0,页面上没有耗时字段):calcSingle(1000) 端到端 1.020 ms;最贵的是 900 到 1000 那一档,102.485 ms、往 DOM 写 334650 字符,因为第 202 行对每个 i 从头重算。直调函数在 1000、5000、10000 上是 0.446、6.644、20.721 ms,n 涨 10 倍耗时涨 46 倍。
过程展示那 5898 个字符,一行都没截
第 159 到 162 行是无条件 parts.join(' × '),全文唯一的 slice 在第 123 行、只剥负号:1000! 的过程行 5898 字符、「 × 」恰好 999 个、整块 innerHTML 9460 字符,全档 hasEllipsis=false。要挑刺就挑文案那串——5! 的过程块是 DOM 里的两个 div,第 162 行只到「× 1」,= 120 在第 163 行那个里,连续写法并不存在。
Q:那 0~1000 之间到底哪里会算错?
页面里不会,它只有 BigInt 一条乘法路,n = 0..1000 全档与公式核验 0 处不一致。会错的是复核它的那一步:Number 从 23! 起丢精确,171! 直接 Infinity。
Q:尾随零个数页面上看得到吗?
看不到,统计块只有 n、结果位数、是否超长结果 3 项(第 168 到 170 行),超长阈值是严格大于 20 位,21! 还是「否」、22! 才「是」。249 是脚本数出来的。
Q:想一次拿到 0! 到 1000! 的整列怎么办?
范围模式做不到,第 194 行按差值拒绝,能过的最大跨度是 101 项。真要长清单,就在阶乘计算器的单值框里按档逐个取,最大一档是 2568 位。
如果你是为排列组合在攒阶乘,可以先看排列与组合那篇——把分子分母约掉之前先算出裸值是常见翻车点;同一类「先乘后除」的坑在最小公倍数那篇里也出现过,中间量比结果先溢出。