17 道题彻底理解 JavaScript 中的类型转换
Category(分类): JavaScript Status: 已更新
类型转换是 JavaScript 运行时语义的基础。它既出现在 String()、Number() 这样的显式转换中,也出现在 +、==、比较运算、属性访问和条件判断中。
JavaScript 有 7 种原始类型:undefined、null、boolean、number、bigint、string、symbol,以及对象类型。原文把转换总结为 ToString、ToBoolean 和 ToNumber,这有助于入门,但现代规范还需要区分 ToPrimitive、ToNumeric 和 BigInt 相关规则。
一、先看 17 道题
下面每道题都建议先自己判断,再查看后面的解析:
true + false
;12 / '6'
;'number' + 15 + 3
;15 + 3 + 'number'
;[1] > null
;'foo' + + 'bar'
;'true' == true
;false == 'false'
;null == ''
;!!'false' == !!'true'
;['x'] == 'x'
;[] + null + 1
;[1, 2, 3] == [1, 2, 3]
;{} + [] + {} + [1]
;! + [] + [] + ![]
;new Date(0) - 0
;new Date(0) + 0
二、显式转换与隐式转换
1. 显式转换
String(123)
Number('123')
Boolean(0)
BigInt('123')
BigInt() 只能把符合规则的整数表示转换为 BigInt,不能把小数直接转换为 BigInt。
2. 隐式转换
隐式转换由具体语法触发:
if、while、逻辑非等使用 ToBoolean;- 算术运算通常使用 ToNumeric,再根据 Number/BigInt 规则运算;
+可能执行字符串拼接,也可能执行数值加法;<、>等关系比较会按规范进行 ToPrimitive 和数值/字符串比较;==有专门的抽象相等比较算法;- 对象参与运算时,通常先执行 ToPrimitive。
严格相等 === 不会为了比较两边而执行一般的隐式类型转换,但 NaN !== NaN、+0 === -0 等特殊结果仍要注意。
三、ToString 与字符串拼接
String(123) // '123'
String(null) // 'null'
String(undefined) // 'undefined'
String(true) // 'true'
String(Symbol('id')) // 'Symbol(id)'
123 + '' // '123'
'age: ' + 18 // 'age: 18'
Symbol 可以通过 String(symbol) 显式转换,但不能通过普通字符串拼接隐式转换:
String(Symbol('id')) // 'Symbol(id)'
// '' + Symbol('id') // TypeError
+ 从左到右计算,只要某一步已经得到字符串,后续的 + 往往会继续进行字符串拼接。
四、ToBoolean:真值与假值
JavaScript 中会转换为 false 的值只有:
false;0、-0、0n;NaN;'';null;undefined。
其他对象都是真值,包括空数组、空对象、空函数和 new Boolean(false):
Boolean('') // false
Boolean(0) // false
Boolean(0n) // false
Boolean(NaN) // false
Boolean(null) // false
Boolean(undefined) // false
Boolean([]) // true
Boolean({}) // true
Boolean(Symbol()) // true
&& 和 || 会使用真值判断,但返回的是操作数本身:
const value = 'hello' && 123
console.log(value) // 123
const fallback = '' || 'default'
console.log(fallback) // 'default'
五、ToNumber 与 ToNumeric
Number('123') // 123
Number(' 12 ') // 12
Number('') // 0
Number('\n') // 0
Number('12s') // NaN
Number(null) // 0
Number(undefined) // NaN
Number(true) // 1
Number(false) // 0
一元加号也会执行 Number 转换:
+'123' // 123
+'' // 0
+'bar' // NaN
减法、乘法、除法、取模和按位运算通常会触发数值相关转换;关系比较会先进行原始值转换,若两边都是字符串则按字符串顺序比较,否则通常进行数值比较;+ 若有字符串操作数,可能走字符串拼接。
Symbol 不能隐式或显式转换为 Number:
// Number(Symbol('id')) // TypeError
// +Symbol('id') // TypeError
BigInt 与 Number 不能直接混合算术:
1n + 2n // 3n
// 1n + 2 // TypeError
六、对象如何转换为原始值
对象参与 +、比较或显式转换时,通常先经过 ToPrimitive。对象可以通过 Symbol.toPrimitive 自定义转换:
const price = {
value: 10,
[Symbol.toPrimitive](hint) {
if (hint === 'string') return `$${this.value}`
return this.value
}
}
console.log(String(price)) // $10
console.log(price + 5) // 15
如果对象没有 Symbol.toPrimitive,OrdinaryToPrimitive 会根据 hint 尝试 valueOf() 和 toString():
- hint 为
number:通常先valueOf(),再toString(); - hint 为
string:通常先toString(),再valueOf(); - hint 为
default:普通对象通常按 number 方向处理,Date等对象有特殊默认提示行为。
因此不能简单说“所有对象都是先 valueOf 再 toString”,也不能把 + 的默认提示固定描述成 number。
const object = {
valueOf() {
return 10
},
toString() {
return 'object'
}
}
object - 1 // 9
object + 1 // 11(该对象的 valueOf() 返回 10)
对数组来说,valueOf() 通常返回数组本身,随后通过 toString() 得到逗号连接的字符串:
String([]) // ''
String([1, 2, 3]) // '1,2,3'
Number([]) // 0
Number([10]) // 10
Number([1, 2]) // NaN
七、17 道题解析
1. true + false
+ 在没有字符串操作数时进行数值运算:
true + false // 1 + 0 -> 1
2. 12 / '6'
除法会把字符串转换为 Number:
12 / '6' // 12 / 6 -> 2
3. 'number' + 15 + 3
从左到右计算,第一步已经是字符串拼接:
'number' + 15 + 3 // 'number153'
4. 15 + 3 + 'number'
前两个操作数先进行数值加法,之后再拼接字符串:
15 + 3 + 'number' // '18number'
5. [1] > null
关系比较先把两边转换为原始值和数值:
[1] > null
// '1' > 0
// 1 > 0
// true
6. 'foo' + + 'bar'
一元加号优先于二元加号:
'foo' + + 'bar'
// 'foo' + NaN
// 'fooNaN'
7. 'true' == true
抽象相等比较会把布尔值转成数字,把字符串 'true' 转数字得到 NaN:
'true' == true // NaN == 1 -> false
8. false == 'false'
同理,false 转成 0,字符串 'false' 转数字得到 NaN:
false == 'false' // 0 == NaN -> false
9. null == ''
null 只与 undefined 或自身宽松相等,不会因为 Number(null) === 0 就等于空字符串:
null == '' // false
null == undefined // true
null == 0 // false
10. !!'false' == !!'true'
两个非空字符串都是真值,两个 !! 都得到 true:
!!'false' == !!'true' // true == true -> true
11. ['x'] == 'x'
数组先转换为原始值 'x',两边都是字符串:
['x'] == 'x' // true
12. [] + null + 1
空数组转换为空字符串:
[] + null + 1
// '' + null + 1
// 'null' + 1
// 'null1'
13. [1, 2, 3] == [1, 2, 3]
两边都是对象,抽象相等比较不会把两个同类型对象按内容深比较,它们是两个不同引用:
[1, 2, 3] == [1, 2, 3] // false
14. {} + [] + {} + [1]
这道题依赖代码出现的位置。在浏览器经典脚本的语句位置,开头的 {} 可能被解析成空代码块,表达式从 +[] 开始:
{} + [] + {} + [1]
// 可能得到 '0[object Object]1'
如果强制把它写成表达式,结果不同:
({} + [] + {} + [1])
// '[object Object][object Object]1'
面试中应先说明解析上下文,不能把一个依赖脚本解析位置的结果说成 JavaScript 在所有环境都固定如此。
15. ! + [] + [] + ![]
! + [] + [] + ![]
// !(+[]) + [] + (![])
// !0 + [] + false
// true + '' + false
// 'truefalse'
16. new Date(0) - 0
减法要求数值,Date 的数值转换使用时间戳:
new Date(0) - 0 // 0
17. new Date(0) + 0
Date 在默认提示下通常优先使用字符串表示,因此结果是本地化的日期字符串再拼接 '0':
new Date(0) + 0
// 类似 'Thu Jan 01 1970 ...0'
具体日期文本、时区和语言环境由运行环境决定,不应把某个机器上的 GMT+0200 输出写成固定答案。
八、补充:几个常见误区
0 || '0' && {}
// '0' && {}
// {}
&& 优先级高于 ||,并且逻辑运算返回操作数本身,不是强制返回布尔值。
推荐业务代码:
- 优先使用
===和!==; - 需要默认值时区分
||与??; - 需要数字时显式
Number()并验证Number.isFinite(); - 需要解析用户输入时不要依赖隐式转换;
- 不要用
eval、new Function或字符串拼接模拟安全解析; - 通过单元测试覆盖
null、undefined、NaN、空字符串、BigInt、Symbol 和对象自定义转换。