技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

通过面试题研究 JavaScript 数据类型转换

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/128-通过面试题研究JavaScript数据类型转换
本文目录7 个章节
  1. 前言
  2. JavaScript 数据类型
  3. 类型转换的基本抽象操作
  4. == 两边的隐式转换
  5. 需要注意的 {} 和 +
  6. 回到开头的题目
  7. 现代实践建议

通过面试题研究 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)

原始值类型包括:

  • number
  • string
  • boolean
  • null
  • undefined
  • symbol
  • bigint

对象是另一种语言级类型。数组、正则表达式、日期、错误对象、MapSetNumber/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 的细节不同:parseIntparseFloat 是按字符串解析,不等价于 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 值包括:

  • false
  • 0-0NaN
  • 0n
  • ''
  • null
  • undefined

浏览器还存在历史遗留的 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

parseIntparseFloat 是字符串解析函数,规则不同:

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。具体优先顺序受提示类型影响;以数值转换为例,通常是:

  1. 调用对象的 Symbol.toPrimitive(如果存在)。
  2. 若没有得到原始值,调用 valueOf()
  3. 若仍没有得到原始值,调用 toString()
  4. 如果最终没有得到原始值,则抛出 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.isNumber.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

逐步分析:

  1. 100 + true:没有字符串,true 转为 1,得到 101
  2. 101 + 21.2:都是 Number,得到 122.2
  3. 122.2 + nullnull 转为 0,仍是 122.2
  4. 122.2 + undefinedundefined 转为 NaN,得到 NaN
  5. NaN + 'Tencent':存在字符串,NaN 转为 'NaN',得到 'NaNTencent'
  6. 'NaNTencent' + [][] 转为空字符串,结果仍是 'NaNTencent'
  7. 再加 null,字符串化为 'null',得到 'NaNTencentnull'
  8. 再加 9,字符串化为 '9',得到 'NaNTencentnull9'
  9. 再加 false,字符串化为 'false',最终得到 'NaNTencentnull9false'

现代实践建议

  1. 业务代码尽量使用 ===,只有在明确理解 == 规则且确实需要时才使用宽松相等。
  2. 表单输入通常是字符串,计算前显式校验并使用 NumberparseInt(value, 10) 或专门的 schema 校验,不要依赖 +value 的隐式转换。
  3. 判断 NaN 使用 Number.isNaN,判断有限数使用 Number.isFinite;不要使用全局 isNaN 的隐式转换版本。
  4. 金额、计数器等超过安全整数范围的值考虑 BigInt 或十进制定点库,并避免 BigInt 与 Number 混合运算。
  5. 对象自定义 Symbol.toPrimitive 会影响日志、拼接、比较和运算,公共库应谨慎使用并写清楚契约。

参考资料

  1. ECMAScript Language Specification:Type Conversion
  2. MDN:Expressions and operators
  3. MDN:相等性比较和相同性

作者:立花正仁

链接:https://juejin.cn/post/6956170676327677966

来源:稀土掘金。著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS