两种类型转换
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。
假值有:
false0、-0、0n""(空字符串)nullundefinedNaN
就这些。其他所有值都是真值,包括 "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 把两者都转换成字符串。数组转换成字符串时会用逗号连接元素,空数组连接的结果是 ""。所以 [] + [] 变成 "" + "",也就是 ""。这是个好玩的小把戏,也是永远不要对非原始值使用 + 的好理由。