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

显示模式

登录
ARCHIVE DOCUMENTJS

17 道题彻底理解 JavaScript 中的类型转换

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/03-17道题彻底理解 JavaScript 中的类型转换
本文目录9 个章节
  1. 一、先看 17 道题
  2. 二、显式转换与隐式转换
  3. 三、ToString 与字符串拼接
  4. 四、ToBoolean:真值与假值
  5. 五、ToNumber 与 ToNumeric
  6. 六、对象如何转换为原始值
  7. 七、17 道题解析
  8. 八、补充:几个常见误区
  9. 参考资料

17 道题彻底理解 JavaScript 中的类型转换

Category(分类): JavaScript Status: 已更新

类型转换是 JavaScript 运行时语义的基础。它既出现在 String()Number() 这样的显式转换中,也出现在 +==、比较运算、属性访问和条件判断中。

JavaScript 有 7 种原始类型:undefinednullbooleannumberbigintstringsymbol,以及对象类型。原文把转换总结为 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. 隐式转换

隐式转换由具体语法触发:

  • ifwhile、逻辑非等使用 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-00n
  • 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.toPrimitiveOrdinaryToPrimitive 会根据 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()
  • 需要解析用户输入时不要依赖隐式转换;
  • 不要用 evalnew Function 或字符串拼接模拟安全解析;
  • 通过单元测试覆盖 nullundefinedNaN、空字符串、BigInt、Symbol 和对象自定义转换。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS