Menu

JavaScript 类型转换:隐式转换与显式转换

JavaScript 如何在类型之间转换值:容易让人踩坑的隐式规则,你应该优先使用的显式转换,以及它们各自何时发生。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

两种类型转换

JavaScript 对类型很宽松。当运算符拿到“错误”类型的值时,语言不会抛出错误,而是进行转换。这种转换叫作类型转换(coercion),分为两种:

  • 显式转换:你主动要求转换,比如 Number("42")、String(99)、Boolean(value)。
  • 隐式转换:运算符在你没有写转换代码时触发转换,比如 "5" - 2、"" == 0、if (value)。

两者最终调用的都是同一套底层转换规则,区别只在于是你决定转换,还是语言替你决定。几乎所有令人困惑的 JavaScript 结果,比如 "5" + 1 === "51"、[] == false、null == undefined,都来自让你措手不及的隐式转换。

思路是这样的:如果你写了 Number(...) 或 String(...),你清楚自己要的是什么。如果你没写,每个运算符对该怎么做都有自己的主张,而 bug 就藏在这些主张里。

三种目标类型

类型转换的结果总是三种原始类型之一:string、number 或 boolean。(还有第四种 bigint,但其他类型永远不会被自动转换成它。)其他规则都取决于运算符要转换成哪种目标类型。

字符串转换最宽容,每个值都有字符串形式。注意,对象会被转换成毫无用处的 "[object Object]",所以把对象和字符串拼接起来打印("user: " + user)几乎从来显示不出你想要的内容。请改用 JSON.stringify,或者在模板字符串里写出具体的字段。

转换为数字

数字转换更严格。字符串必须真的像一个数字,否则就会得到 NaN:

括号里标出的那些意外情况值得记住:

  • 空字符串和只包含空白的字符串会变成 0,而不是 NaN。
  • null 变成 0,但 undefined 变成 NaN。
  • 空数组变成 0;只有一个元素的数组会转换它的那个元素;有多个元素的数组变成 NaN。

如果你想从带有多余字符的字符串里解析出数字,请用 parseInt 或 parseFloat,而不是 Number:

调用 parseInt 时一定要传入进制(10)。如果不传,以 "0x" 开头的字符串会被当作十六进制解析,而这很少是你想要的。

转换为布尔值

布尔转换是三种中最简单的。只有一小部分值会转换为 false,其他一切都是 true。

假值有:

  • false
  • 0、-0、0n
  • ""(空字符串)
  • null
  • undefined
  • NaN

就这些。其他所有值都是真值,包括 "false"、"0"、[] 和 {}。

空数组和空对象的结果常常让从 Python 转过来的人吃亏,因为在 Python 中空集合是假值。在 JavaScript 中它们是真值:如果你想判断“这个数组是空的吗?”,请显式地检查 arr.length === 0。

只要值出现在需要布尔值的地方,就会发生布尔转换:if (...)、while (...)、三元运算符 ? :,以及逻辑运算符 &&、||、!。

+ 运算符是特例

大多数算术运算符都会把操作数强制转换成数字。+ 不一样:只要任意一边是字符串,+ 就做字符串拼接,否则做数值加法。

计算是从左到右进行的。1 + 2 + "3" 先算出 1 + 2 = 3,再算 3 + "3" = "33"。而 "1" + 2 + 3 一开始就是字符串,之后一直是字符串:先得到 "12",再得到 "123"。

这就是用 + 拼接字符串很脆弱的原因。模板字符串没有这个问题:

模板字符串把 count + 1 当作一个独立的数值表达式来计算,然后再把结果插入字符串,不会发生意外的类型转换。

优先使用显式转换

当你确实需要转换类型时,就明确地写出来。这只多花几个字符,却能让下一个读代码的人完全没有疑问:

布尔值也一样。!!value 能用,也很常见,但 Boolean(value) 准确地说明了它在做什么:

一条合理的规则:在业务逻辑中使用显式转换,把简写形式(+x、!!x)留给那些需要简洁、而且从上下文就能看出意图的地方。

== 运算符高度依赖类型转换

相等运算符是类型转换意外的最大来源。== 会先转换再比较;=== 不会。

上面每一个 true,都是一连串多步转换的结果,大多数开发者都没法凭记忆说出来。问题就在这里:碰巧能运行的代码,迟早会出问题。下一页我们会详细讲解完整的相等规则;现在只需记住:默认使用 ===,只在 x == null 这个特定惯用写法中使用 ==(它能同时匹配 null 和 undefined)。

综合示例

下面这个例子展示了类型转换在哪里有帮助、在哪里有害:

注意第二次调用。Number("") 返回 0 而不是 NaN,所以 parsePrice("") 返回 0,而这多半不是使用这个函数的人想要的。如果空输入应该被拒绝,就加一个显式检查:

知道哪些值会转换成 0、哪些会转换成 NaN,正是能帮你避免下游隐蔽 bug 的那种知识。

要点总结

  • 类型转换会根据运算符,把值转换成 string、number 或 boolean。
  • + 只要遇到字符串就变成拼接;其他所有算术运算符都会转换成数字。
  • 假值是一份固定的简短列表,把它记住。其他一切都是真值,包括 [] 和 {}。
  • Number("") 是 0,Number([]) 是 0,Number(null) 是 0,但 Number(undefined) 是 NaN。这些都会出现在真实的 bug 中。
  • 优先使用 Number(x)、String(x)、Boolean(x),而不是巧妙的隐式技巧。将来的你会感谢现在的自己。

下一节:相等运算符

类型转换在比较中做的所有事情都发生在 == 里。下一页会讲解 ==、=== 和 Object.is 的区别,== 仍然有用的那一种惯用写法,以及为什么 linter 默认会标记宽松的写法。

常见问题

JavaScript 中的类型转换是什么?

类型转换就是 JavaScript 自动把一个值从一种类型转换成另一种类型:数字变成字符串,字符串变成数字,或者任何值变成布尔值。当 +、== 或 if 这样的运算符和语句遇到不是预期类型的值时,会发生隐式转换;你自己调用 Number(x)、String(x) 或 Boolean(x) 时,就是显式转换。

隐式转换和显式转换有什么区别?

显式转换是你有意调用转换函数:Number("42")、String(99)、Boolean(value)。隐式转换是运算符在你不知情时触发的转换:"5" - 2 得到 3,"5" + 2 得到 "52"。显式转换易读且可预测;大多数“为什么这里是 NaN”的 bug 都来自隐式转换。

如何在 JavaScript 中把字符串转换成数字?

用 Number("42") 做严格转换(如果字符串不是有效数字就返回 NaN);如果想从一个更杂乱的字符串开头读出数字,就用 parseInt("42px", 10) 或 parseFloat("3.14em")。一元 + 运算符(+"42")的作用和 Number() 相同,但快速浏览代码时更容易被忽略。

为什么 [] + [] 返回空字符串?

+ 运算符对两个数组没有有意义的数值运算,所以 JavaScript 把两者都转换成字符串。数组转换成字符串时会用逗号连接元素,空数组连接的结果是 ""。所以 [] + [] 变成 "" + "",也就是 ""。这是个好玩的小把戏,也是永远不要对非原始值使用 + 的好理由。

Coddy 编程语言插图

和 Coddy 一起学编程

开始