通过面试题研究 JavaScript 数据类型转换
Category(分类): JavaScript Status: 未知
JavaScript 是动态类型语言:变量绑定本身不要求固定类型,运行时的值可以在不同操作中参与不同的转换。准确理解转换规则,比记忆大量“面试题口诀”更重要。
前言
先来看一道题:
const result = 100 + true + 21.2 + null + undefined + 'Tencent' + [] + null + 9 + false
console.log(result)
// NaNTencentnull9false
题目考察的是隐式类型转换。要做对它,需要知道 + 在不同操作数下可能是数值加法,也可能是字符串拼接;对象还可能先经过原始值转换。
JavaScript 数据类型
JavaScript 的值分为两大类:原始值(primitive)和对象(object)。
原始值类型包括:
numberstringbooleannullundefinedsymbolbigint
对象是另一种语言级类型。数组、正则表达式、日期、错误对象、Map、Set、Number/String/Boolean 包装对象等都属于对象;函数是具有可调用内部方法的对象,typeof 对函数返回字符串 "function",但规范并没有额外定义一个名为 function 的值类型。
Math 也是普通的内置对象,不是“特殊数据类型”。typeof null 返回 "object" 是历史遗留行为,不代表 null 真的是对象。
类型转换的基本抽象操作
类型转换不只有“转成 Number、String、Boolean”三种。规范中常见的抽象操作包括:
ToBoolean:把值用于条件和逻辑判断ToNumber:数值运算和比较需要的数字转换ToString:字符串拼接或显式字符串化ToPrimitive:先把对象转换成原始值ToBigInt:在 BigInt 相关操作中转换
开发者可以通过 Number(value)、String(value)、Boolean(value)、BigInt(value) 等显式调用,但不同 API 的细节不同:parseInt 和 parseFloat 是按字符串解析,不等价于 Number。
转换为 Boolean
显式转换:
console.log(Boolean(2)) // true
console.log(Boolean('')) // false
console.log(!!2) // true
隐式转换通常发生在条件语句和逻辑运算符中:
if (2) {
console.log('条件成立')
}
const value = 'hello' && 123
console.log(value) // 123
&& 和 || 会在内部使用 ToBoolean 判断,但返回的是原始操作数,而不是强制返回布尔值:
console.log(2 || 'hello') // 2
console.log('' || '默认值') // 默认值
console.log('hello' && 123) // 123
常见 falsy 值包括:
false0、-0、NaN0n''nullundefined
浏览器还存在历史遗留的 document.all 特殊行为。除了这些值,普通 JavaScript 值通常是真值,包括空数组 []、空对象 {} 和字符串 '0'。
转换为 String
显式转换:
console.log(String([1, 2, 3])) // '1,2,3'
console.log(String({})) // '[object Object]'
console.log(String(Symbol('id'))) // 'Symbol(id)'
隐式字符串转换最常见于二元 +:如果两个操作数经过原始值转换后至少有一个是字符串,就执行字符串拼接;“另一边是对象”并不意味着一定拼接,因为对象可能先转换成数字原始值:
console.log(1 + '123') // '1123'
console.log(1 + {}) // '1[object Object]'
console.log([] + 1) // '1',[] 转成空字符串
console.log([1] + 1) // '11',[1] 转成字符串 '1'
console.log([] - 1) // -1,减法会走数值转换
注意,'' + Symbol() 会抛出 TypeError,但 String(Symbol()) 是专门允许的显式转换。
转换为 Number
显式转换:
console.log(Number('')) // 0
console.log(Number('10px')) // NaN
console.log(Number('10')) // 10
console.log(Number(true)) // 1
console.log(Number(false)) // 0
console.log(Number(null)) // 0
console.log(Number(undefined)) // NaN
parseInt、parseFloat 是字符串解析函数,规则不同:
console.log(parseInt('10px', 10)) // 10
console.log(Number('10px')) // NaN
console.log(parseFloat('3.14rem')) // 3.14
Symbol 不能隐式或显式转换为 Number:
try {
Number(Symbol('id'))
} catch (error) {
console.log(error instanceof TypeError) // true
}
显式 Number(bigint) 是允许的,但如果 BigInt 超出安全整数范围,可能丢失精度;BigInt 和 Number 不能直接进行大多数算术运算:
const big = 12312412321312312n
console.log(Number(big)) // 可能显示为近似的 Number 值
console.log(Number.isSafeInteger(Number(big))) // false
console.log(BigInt('12312412321312312')) // 精确的 BigInt
try {
// 1n + 1 会抛出 TypeError,必须先明确转换
1n + 1
} catch (error) {
console.log(error instanceof TypeError) // true
}
对象如何转换为原始值
在需要原始值的运算中,对象通常先经过 ToPrimitive。具体优先顺序受提示类型影响;以数值转换为例,通常是:
- 调用对象的
Symbol.toPrimitive(如果存在)。 - 若没有得到原始值,调用
valueOf()。 - 若仍没有得到原始值,调用
toString()。 - 如果最终没有得到原始值,则抛出
TypeError;得到字符串后再按Number规则转换时,可能得到NaN。
const obj = {
name: 'xxx'
}
console.log(obj - 10) // NaN
// 数值运算会尝试把 obj 转成原始值:
// valueOf() 返回对象本身,toString() 返回 '[object Object]',再转 Number 得到 NaN
对象可以自定义转换:
const price = {
value: 10,
[Symbol.toPrimitive](hint) {
return hint === 'string' ? '$10' : this.value
}
}
console.log(price + 5) // 15
console.log(String(price)) // '$10'
隐式数值转换常见于:
>、<、<=、>=等关系比较|、&、^、~等按位运算-、*、/、%等算术运算- 一元
+ - 二元
+在没有字符串原始值时
== 两边的隐式转换
== 使用 Abstract Equality Comparison,规则分支较多,不能简单概括为“所有值都先转数字”。生产代码通常优先使用 ===,但理解以下典型情况仍有助于阅读旧代码。
对象和字符串
对象先转换为原始值,再与字符串比较:
console.log([1, 2, 3] == '1,2,3') // true
console.log([1, 2, 3].valueOf()) // [1, 2, 3]
console.log([1, 2, 3].toString()) // '1,2,3'
null 和 undefined
null 只与 undefined 宽松相等:
console.log(null == undefined) // true
console.log(null === undefined) // false
console.log(null == 0) // false
console.log(undefined == false) // false
对象和对象
两个不同的对象即使内容相同,也比较引用身份;只有指向同一个对象时才相等:
console.log({} == {}) // false
const a = {}
const b = a
console.log(a == b) // true
NaN
NaN 与任何值(包括自身)使用 == 或 === 都不相等:
console.log(NaN == NaN) // false
console.log(NaN === NaN) // false
console.log(Number.isNaN(NaN)) // true
console.log(Object.is(NaN, NaN)) // true
其他常见例子
console.log(0 == false) // true
console.log('' == false) // true
console.log([] == 0) // true,[] 先转成 '',再转成 0
console.log([1] == 1) // true
console.log(0n == 0) // true,但 0n + 0 会抛错
这些结果正是 == 规则复杂的原因。若业务需要明确类型和边界,使用 ===、Object.is、Number.isNaN 等更清晰的 API。
需要注意的 {} 和 +
原文中的 {} + [] === 0 容易误导,因为 {} 在语句开头可能被解析为一个空代码块,后面的 +[] 才是对空数组进行一元数值转换;放在表达式或函数参数中时,解析结果不同:
// 表达式上下文:对象和数组先转原始值,再进行字符串拼接
console.log(({} + []) === 0) // false
console.log([] + {}) // '[object Object]'
// 一元 + 会把空数组转换为数字 0
console.log(+[]) // 0
// 经典脚本中单独写在语句开头时,{} 可能被解析为空代码块
// {} + [] 可能得到 0;不要把这种解析差异当作可靠业务逻辑
回到开头的题目
const result = 100 + true + 21.2 + null + undefined + 'Tencent' + [] + null + 9 + false
console.log(result) // NaNTencentnull9false
逐步分析:
100 + true:没有字符串,true转为1,得到101。101 + 21.2:都是 Number,得到122.2。122.2 + null:null转为0,仍是122.2。122.2 + undefined:undefined转为NaN,得到NaN。NaN + 'Tencent':存在字符串,NaN转为'NaN',得到'NaNTencent'。'NaNTencent' + []:[]转为空字符串,结果仍是'NaNTencent'。- 再加
null,字符串化为'null',得到'NaNTencentnull'。 - 再加
9,字符串化为'9',得到'NaNTencentnull9'。 - 再加
false,字符串化为'false',最终得到'NaNTencentnull9false'。
现代实践建议
- 业务代码尽量使用
===,只有在明确理解==规则且确实需要时才使用宽松相等。 - 表单输入通常是字符串,计算前显式校验并使用
Number、parseInt(value, 10)或专门的 schema 校验,不要依赖+value的隐式转换。 - 判断
NaN使用Number.isNaN,判断有限数使用Number.isFinite;不要使用全局isNaN的隐式转换版本。 - 金额、计数器等超过安全整数范围的值考虑
BigInt或十进制定点库,并避免 BigInt 与 Number 混合运算。 - 对象自定义
Symbol.toPrimitive会影响日志、拼接、比较和运算,公共库应谨慎使用并写清楚契约。
参考资料
作者:立花正仁
链接:https://juejin.cn/post/6956170676327677966
来源:稀土掘金。著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。