侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 104 篇文章
  • 累计收到 7 条评论

parseInt 把 0.0000005 读成 5

2026-9-23 / 0 评论 / 8 阅读

parseInt 把 0.0000005 读成 5

0.0000005 交给 parseInt,返回 5。同一个值交给 Number,返回 5e-7。两个函数号称都在把输入变成数字,输入也是同一个,答案差了一百万倍。

parseInt(0.0000005); // 5
Number(0.0000005); // 5e-7

写出来的 0.00000055e-7 在 JS 里是同一个数,字面量只是写法不同。所以上面这一对结果不能怪调用者传错了东西。

规范里 parseInt 的第一步是 ToString

ECMA-262 描述 parseInt 时,第一步就是对参数做 ToString(x),之后的解析全部发生在得到的字符串上。传数字还是传字符串不改变路径:数字先变成一个字符串,再从这个字符串里找最长前缀。

数字转字符串这一步,规范不要求保住手写的写法,只要求给出最短的、能往返读回同一个数的那一串。0.0000005 的往返表示是 "5e-7"。parseInt 从里面认到 5 就停,e 和后面的全部丢掉,返回 5。

两条算法:前缀匹配和整串匹配

Number 走 ToNumber 里的 StringToNumber,用 StringNumericLiteral 这套语法:整串必须正好是一个数值字面量,指数和小数点都合法,多一个字符就整串判废,返回 NaN。

parseInt 要求的是开头有一段合法的数字前缀,尾部有什么都不管。这个差别在两个方向上都留下后果。parseInt("12px") 给 12,Number("12px") 给 NaN。反过来,parseInt("") 给 NaN,Number("") 给 0,因为空串在 StringNumericLiteral 语法里有定义,值是 0。

--- 前缀匹配 vs 整串匹配 ---
"12px"     Number: NaN      parseInt: 12       parseFloat: 12
"12 34"    Number: NaN      parseInt: 12       parseFloat: 12
"3.9abc"   Number: NaN      parseInt: 3        parseFloat: 3.9
"0x1f "    Number: 31       parseInt: 31       parseFloat: 0
" -7"      Number: -7       parseInt: -7       parseFloat: -7

parseFloat 是第三条路:它认小数点,也不要求整串合法,但遇到 0x 就停,所以 parseFloat("0x1f ") 是 0。

十五个用例,十一个不一致

同一批输入同时喂给两个函数,结果摆在一起看更清楚。下面这张表是本机跑出来的,中间那列是 String(x),也就是 parseInt 眼里真正的输入。

input                                   String(x)            Number(x)        parseInt(x)
------------------------------------------------------------------------------------------------------------
5e-7  极小浮点数                             "5e-7"               5e-7             5             <-- 不一样
1e+21  极大浮点数                            "1e+21"              1e+21            1             <-- 不一样
1e-21  极小浮点数(另一个量级)                     "1e-21"              1e-21            1             <-- 不一样
"0x10"  十六进制字符串                         "0x10"               16               16
"1e3"  普通科学计数法字符串                       "1e3"                1000             1             <-- 不一样
"  12  "  前后有空白                         "  12  "             12               12
""  空串                                  ""                   0                NaN           <-- 不一样
"08"  前导零                               "08"                 8                8
"1_000"  数字分隔符形如字符串                     "1_000"              NaN              1             <-- 不一样
"Infinity"  Infinity 字符串                "Infinity"           Infinity         NaN           <-- 不一样
null  null                              "null"               0                NaN           <-- 不一样
undefined  undefined                    "undefined"          NaN              NaN
"12px"  数字开头 + 单位                       "12px"               NaN              12            <-- 不一样
" 0b101"  二进制字面量                        " 0b101"             5                0             <-- 不一样
true  布尔                                "true"               1                NaN           <-- 不一样

输入里有 15 个用例,Number 与 parseInt 结果不同的有 11 个:
  5e-7, 1e+21, 1e-21, "1e3", "", "1_000", "Infinity", null, "12px", " 0b101", true

两个值得单独看的行。1e21 那一行里 String 出来的是 "1e+21",parseInt 认到 1 就返回,一个巨大的数字变成了 1。null 那一行,Number(null) 是 0,parseInt(null) 是 NaN,因为 ToString(null) 得到的是字符串 "null",parseInt 找不到数字前缀。

undefined 是唯一两边一致给 NaN 的用例,因为它无论怎么转都读不出数值。

十六进制那一处两边反而一样

radix 省略或者给 0 时,以 0x0X 开头的串按 16 进制处理,parseInt("0x10") 是 16。显式写 parseInt("0x10", 10) 就变成 0:从 0 往后读到 x 停住,前缀是 "0",值是 0。而 Number("0x10") 也是 16。这一个用例上两条路径碰巧给同一个答案,容易被当成可以互换的证据。

--- radix 缺省与显式 10 的区别 ---
"0x10"   parseInt(s) = 16     parseInt(s, 10) = 0      Number(s) = 16
"0X10"   parseInt(s) = 16     parseInt(s, 10) = 0      Number(s) = 16
"010"    parseInt(s) = 10     parseInt(s, 10) = 10     Number(s) = 10
"0o10"   parseInt(s) = 0      parseInt(s, 10) = 0      Number(s) = 8

--- parseInt 的第二参数不合法时 ---
radix = undefined  parseInt('10', radix) = 10
radix = 0          parseInt('10', radix) = 10
radix = 1          parseInt('10', radix) = NaN
radix = 10         parseInt('10', radix) = 10
radix = 16         parseInt('10', radix) = 16
radix = 37         parseInt('10', radix) = NaN
radix = -1         parseInt('10', radix) = NaN
radix = 2.5        parseInt('10', radix) = 2

radix 给 2.5 时被截成 2,所以 parseInt("10", 2.5) 是二进制的结果 2。给 37 或负数直接 NaN。

解析输入还是校验输入

分场景看,选择其实很固定。

解析那种带单位、带尾巴的脏值,要的就是前缀行为,比如从 "12px" 里取宽度、从 "3.9abc" 里取版本号,这时用 parseInt 或 parseFloat,radix 显式写出来,别靠缺省值。判断一个字符串整体是不是合法数字,要的是整串匹配,用 Number,失败判断用 Number.isNaN(x)。全局 isNaN 会先对参数做一次 ToNumber,isNaN("") 是 false,看着像通过了校验。

手里已经是数字、只是想取整的情况,用 Math.truncMath.floor,不要绕回 parseInt。这十五个用例里最容易被漏掉的是 1e21 那一行:一个本来很大的产值或时间戳,经过一次 parseInt 就变成了 1,而且不报错。取整这类需求我不用 parseInt,多绕一层字符串没有任何好处。

--- 数值先取整还是先转字符串 ---
3.7            String(x) = "3.7"                      parseInt(x) = 3
-3.7           String(x) = "-3.7"                     parseInt(x) = -3
2147483648     String(x) = "2147483648"               parseInt(x) = 2147483648
1.9e+21        String(x) = "1.9e+21"                  parseInt(x) = 1
NaN            String(x) = "NaN"                      parseInt(x) = NaN
Infinity       String(x) = "Infinity"                 parseInt(x) = NaN

--- 单独看最反直觉的那个 ---
tiny = 5e-7
String(tiny) = "5e-7"   (ToString 给的是最短往返表示,不是 0.0000005)
Number('5e-7') = 5e-7
parseInt('5e-7') = 5   (认到 5 就停,'e' 及之后全丢)
parseInt(tiny) = 5   (规范第一步是 ToString(x),所以等于上一行)

上面每段输出都在 node v26.8.1 上跑过,两个函数的差异不随版本变化,规范里就是两条不同的算法。

    🤞 分享