最全的javascript类型转换规则精简总结
JavaScript这门语言饱受诟病的点之一就是类型转换, 有时候js的类型转换机制确实会让新手摸不着头脑. 其实我自己经过大量测试和查找经典资料, 总结学习过js的类型转换机制以后,觉得类型转换并没有大家想得那么难以理解, 反倒觉得很清晰. 刚好年末了, 总结分享一下自己的知识笔记.
本篇文章内容不多, 而且知识点总结全面, 耐心看下去, 然后动手测试总结. 不管你是刚入门js或者是看过一遍js但是没有形成知识体系的同学, 这篇文章都能帮助到你.
1 类型转换的分类
谈到类型转换, 那么我们先简要列一下js的数据类型: 七大原始值类型为string, number, boolean, undefined, null, symbol, bigInt
和引用值类型object
.
另外, 我们需要时刻注意, js的类型转换只有三种类型的转换:
- to string
- to boolean
- to number
即原始数据类型{string, number, boolean, undefined, null} + 引用数据类型{object, ... } —to→ {string, boolean, number}的类型转换.
而在这三种类型转换当中, 分为两大块: 显式类型转换和隐式类型转换. 其中, 显式类型转换是隐式类型转换的基础, 隐式类型转换就是在某些场景下隐式地按照显式类型转换的规则类型转换, 所以着重掌握显式类型转换的机制, 然后再记住哪些场景会触发隐式类型转换即可.
2 显式类型转换机制
这里要讨论的显式类型转换机制指的是: 通过函数 String(<value>), Number(<value>), Boolean(<value>)
进行类型转换的机制:
String(<value>)
: 代表<value>
to string的显式类型转换Number(<value>)
: 代表<value>
to number的显式类型转换Boolean(<value>)
: 代表<value>
to boolean的显式类型转换
我们可以将不同类型的值<value>
作为函数String(<value>) | Number(<value>) | Boolean(<value>)
的参数, 然后观察输出的值的类型来自己总结显式类型转换的规则.
对于显式类型转换的机制, 相信大家都很清楚了, 这里就直接列举一下. 不过对象到{string, number, boolean}
的类型转换机制很多人可能会学的混乱, 所以会单独列一小节详细解释一遍.
2.1 to string的原始类型转换
原始数据类型<value>
通过String(<value>)
进行to string的类型转换规则表:
原始类型 | 类型转换规则 |
---|---|
string | string |
number | “” |
boolean | “true” or “false” |
null | “null” |
undefined | “undefined” |
Note: number和boolean都为原始数据类型, 没有toString()方法, 但是仍然可以以点语法的形式调用toString(), javascript会自动将
<number | boolean>.toString()
解析为String(<number | boolean>)
, 进行显式类型转换
2.2 to number的原始类型转换
原始数据类型<value>
通过Number(<value>)
进行to number的类型转换规则表:
原始类型 | 类型转换规则 |
---|---|
string | - ("", "<空白符>", "<多个空白符>") -> 0 - "<number>" -> <number> - "<16进制数>" -> <16 进制数> -> 10 十进制数 -其他字符串 → NaN |
number | 直接返回number |
boolean | true -> 1, false -> 0 |
null | 0 |
undefined | NaN |
2.3 to boolean的原始类型转换
原始数据类型<value>
通过Boolean(<value>)
进行to number的类型转换规则:
- 转换为
false
""
0 || -0 || NaN
null
undefined
- 转换为
true
- 非空字符串
- 非 0 和非NaN 的数值
2.4 对象 to {string, number, boolean}的 To Primitive类型转换
在第1 章有说过, js的类型转换只有其他类型到{string, number, boolean}的类型转换, 其中对象到{string, number, boolean}的类型转换称作为to primitive类型转换, 即对象转原始数据类型.
在ES5版本, 当我们显式或者隐式地将对象转换为原始值(primitive value)的时候, 通常是默认调用对象自身或者原型链上的toString()
或者valueOf()
方法, 将其转换原始值. 或者可以自定义覆盖对象的这两个方法来控制对象的to primitive行为, 不过不建议这样做.
ES6 为开发者提供了官方的[Symbol.toPrimitve]
接口来自定义对象的to primitive操作, 当对象发生显式或者隐式类型转换操作的时候, 会自动调用其预先定义的[Symbol.toPrimitive]
方法, 同时会忽略对象自身的toString()
和valueOf()
方法.
在下面的内容, 将重点讨论ES6版本中, 对象发生类型转换时的方法调用逻辑. 主要划分为一下几块内容进行分析:
- 对象 to boolean
- 对象 to string | number
- 对象有自定义的
[Symbol.toPrimitive]
方法- 对象 to string
- 对象 to number
- 对象没有自定义的
[Symbol.toPrimitive]
方法- 对象 to string
- 对象 to number
- 对象有自定义的
2.4.1 对象 to boolean
关于对象转为布尔值的机制很简单, 一般情况下, 对象to boolean都是直接转换为true
, 而且不会调用对象的[Symbol.toPrimitive], toString, valueOf
这三个方法.
不过需要注意的是, 在<你不知道的JavaScript_中卷>4.2.3 节中提到, document.all
对象在现代浏览器中转为布尔值的时候为true
,而在老版本的IE浏览器中为false
. 这是一种极特殊情况,在vscode中使用这个对象的时候,会提示已经被弃用了,所以不用在意.
2.4.2 对象to string | number
这小节的内容很重要, 也是初学者比较混乱的点, 因为对于对象发生类型转换时, [Symbol.toPrimitive], toString, valueOf
这三个方法是否调用和调用的顺序的知识不太了解.
在ES5的时候, 对象的类型转换是通过内置或者自定义的toString, valueOf
方法进行to primitive类型转换的.
比如下面的代码中, 是对象 to string的类型转换.
const obj = {
toString() {
console.log('to string called') // to string called
return 1
},
valueOf() {
console.log('value of called')
},
}
let res = String(obj)
console.log(res) // "1"
console.log(typeof res) // string
对象<obj>
to string的类型转换的步骤为:
- 调用对象
<obj>
的toString()
方法, 没有则去原型链上查找- 如果toString返回值为原始值, 对返回值进行原始值 to string的类型转换(2.1 节), 转换后的结果即为对象 to string的类型转换结果
- 如果toString返回值为对象, 那么将调用
<obj>
的valueOf()
方法- 如果返回值为原始值, 对返回值进行原始值to string的类型转换, 转换后的结果即为对象 to string的类型转换结果
- 如果返回值为对象, 则报错 ❌
对象<obj>
to string是先调用toString()
, 并在返回值为对象的时候, 再调用valueOf()
. 而对象<obj>
to number的时候, 调用的顺序相反, 不过其他逻辑基本相同.
对象<obj>
to number的类型转换的步骤为:
- 调用对象
<obj>
的valueOf()
方法, 没有则去原型链上查找- 如果valueOf返回值为原始值, 对返回值进行原始值 to number的类型转换(2.2 节), 转换后的结果即为对象 to string的类型转换结果
- 如果valueOf返回值为对象, 那么将调用
<obj>
的toString ()
方法- 如果返回值为原始值, 对返回值进行原始值to number的类型转换, 转换后的结果即为对象 to number的类型转换结果
- 如果返回值为对象, 则报错 ❌
在es5的时候, 对象到原始值的转换结果依赖其toString()
和valueOf()
方法, 这两个方法可以自定义,也可以是原型方法.
一些JavaScript的内置对象会有自己的内置toString()
和valueOf()
方法, 位于其原型上, 汇总如下:
类型 | toString | valueOf |
---|---|---|
object | "[object <type>]" | 指向自身 |
function | 函数的字符串形式 | 指向自身 |
array | "arr0,arr1,..." 或者 ""(数组为空) | 指向自身 |
date | 包含本地时间信息的字符串 | 从1970年 1 月 1 日开始至今的毫秒数 |
regexp | 正则表达式的字符串表示形式 | 指向自身 |
error | 错误名+错误信息: "<err>.name:<err>.message" | 指向自身 |
在ES6, 开发者可以通过官方提供的[Symbol.toPrimitive]
接口去定义对象 to primitive 的行为. 需要注意的是, 如果自定义了对象的[Symbol.toPrimitive]
的方法, 那么, 当对象发生 to primitive类型转换的时候, 那么只会调用[Symbol.toPrimitive]
方法, 而无视ES5中提供的toString(), valueOf()
方法.
比如下面将对象显式转换为string的代码中, 只有[Symbol.toPrimitive]()
会被调用: 如果返回结果为对象, 则直接报错(不会去调用toString
或者是valueOf
); 如果返回结果为原始值, 则将将该原始值进行to string操作, 作为最终的对象 to primitive结果.
const obj = {
[Symbol.toPrimitive] () {
console.log("to primitive called") // to primitive called
// return {} error!!!
return 1
}
toString() {
console.log("to string called")
}
valueOf() {
console.log("value of called")
}
}
String(obj) // 对象 to primitive(string)
上述代码中, 对象obj to primitive(string)的执行示意图如下:
3 隐式类型转换
当我们从第 2 章了解到js每种类型是如何进行显式类型转换的以后, 对于隐式类型转换, 只需要知道在哪些情况下会触发类型转换, 因为类型转换的规则是和显式类型转换是相同的(经过测试, 对象的隐式类型转换稍微有一些区别)
3.1 隐式类型转换情景汇总
隐式类型类型转换大多发生在操作符当中, 比如宽松相等操作符==
两端的类型不同的时候, 会发生隐式类型转换. 而有时候, 某些函数的参数如果是对象, 也会触发对象的类型转换, 比如isNaN(<val>)
输入的参数<val>
为对象的时候, 会触发对象 to primitive(number)的类型转换.
由于我关于函数触发隐式类型转换的测试不多, 所以暂不讨论.
对于某些运算符, 当A <operator> B
的时候, 如果A
和B
类型不一致, 那么将会触发隐式类型转换, 这些运算符汇总如下:
- 宽松相等运算符
==, !=
- 关系运算符
(>, <, <=, >=)
- 逻辑运算符
(&&, ||, !)
if, while, for, ? : + (condition)
中的条件表达式(condition)
- 加性运算符
+
- 算数运算符
(-, *, /, %)
- 一元
+, -
操作
3.2 宽松相等运算符的隐式类型转换
两等号A == B
的隐式类型转换规则其实不是很难, 个人总结的规则如下:
❌ 不会发生隐式类型转换的情形:
- A和B均为{undefined, null}当中的一种,
A == B -> true
- A为{undefined, null}当中的一种, B为{string, boolean, number, 对象}当中的一种,
A == B -> false
- A和B对象均为对象, 则比较对象的地址是否相等
✅ 发生隐式类型转换的情形, 以及类型转换规则:
- A 和 B均为{string, boolean, number}当中的一种, 且 A 和 B 的类型不一致. 那么,string和boolean都会先转为number, 然后再进行比较
- A 和 B 其中有一个为对象, 另一个为{string, boolean, number}中的一种. 此时一个为对象, 一个为原始值, 当两者进行相等比较的时候, 会对对象进行 to primitive 的类型转换(👉 to primitive的类型转换规则参考2.4节. ❗注意: 在隐式类型转换当中, 规则稍有不同, 注意项见下面).
注意, 在隐式类型转换当中, 如果对象发生to primitive操作
- 总是优先调用[Symbol.toPrimitive]. 如果没有定义则调用valueOf, 在valueOf返回对象的时候, 会继续调用toString. (显式类型转换to string是先toString然后valueOf, to number是先valueOf然后toString, 隐式类型转换to primitive总是先valueOf, 然后toString)
- [Symbol.toPrimitive], toString, valueOf这三个方法在返回原始值的时候, 不会将原始值再转为其他的原始值类型, 而是直接返回结果, 作为对象的to primitive 类型转换结果
两等运算==
隐式类型转换示例
❓示例: [] == ![]
的结果是什么?
💡解答: 结果为true
- 从左到右进行扫描, 首先
![]
触发类型转换, 将[]
转为boolean, 结果为false - 然后判断
[] == false
, 此时两端类型不一致, 因而触发数组对象[]
的to primitive隐式类型转换, 调用[].prototype.valueOf()
返回数组对象自身, 因而继续调用[].prototype.toString()
返回""
- 然后判断
"" == false
, 将""
和false
都转换为number, 再进行比较, 最终为0 == 0
, 结果为true
3.3 关系运算符的隐式类型转换
关系运算A <compare operator> B
的隐式类型转换和转换规则:
- 如果A和B是{string, number, boolean}中的类型, 且 A 和 B 类型不一致, 则会进行类型转换, string和boolean都向number进行转换, 转换以后再进行比较
- 如果 A 或 B 为对象(❗注意这里是 或), 则会进行对象的to primitive类型转换, 将所有对象转换为原始值再进行比较, 参见 上一节的对象to primitive类型转换规则
3.4 逻辑运算符和条件表达式中的隐式类型转换
逻辑运算符和条件表达式的隐式类型转换规则最为简单, 实际上就是将不是boolean类型的值, 按照 2.3 和 2.4.1 节的to boolean类型转换规则进行转换, 然后进一步进行条件判定.
3.5 加性运算符 + 的隐式类型转换
加性运算符算是隐式类型转换当中最常考察的点了, 原因在于它其中糅合了很多类型转换的规则.
加性运算A+B
在以下情景当中会发生隐式类型转换:
- A和B为{number, boolean, null, undefined}(非字符串原始值类型)当中的一种, 且A和B类型不一致, 此时A和B都会进行to number的类型转换(to number的规则参照 2.2节), 然后再进行相加
- A和B其中一个为字符串, 另一个为{number, boolean, null, undefined}(非字符串原始值类型)当中的一种, 那么另一个会进行to string的类型转换(to string的规则参照 2.1 节)
- A或 B 为对象(注意这里是 或)的时候, 则将对象按照 to primitive的隐式类型转换规则(参见 3.2 节)先后进行类型转换, 然后再将类型转换后的原始值再按照上述规则 1 和 2 进行类型转换和相加运算
值得一提的是, 在加性运算符 +
的隐式类型转换当中, 类型转换的顺序是从左到右进行的, 先进行类型转换然后再相加. 比如, undefined + null + "1"
的类型转换顺序为:
- undefined: to number -> NaN
- null: to number -> 0
- NaN + 0 -> NaN
- NaN: to string -> "NaN"
- "NaN" + "1" -> "NaN1"
加性操作符隐式类型转换示例
❓示例: 下面的代码中, result的值为?
let result = 100 + true + 21.2 + null + undefined + "Tencent" + [] + null + 9 + false
解答: "NaNTencentnull9false"
- 从左向右进行扫描, 进行成对进行相加运算
- 100 + true: true to number -> 1, 100 + 1 = 101(规则 1)
- 101 + 21.2 -> 122.2
- 122.2 + null: null to number -> 0, 122.2 + 0 = 122.2(规则 1)
- 122.2 + undefined: undefined to number -> NaN, 122.2 + undefined = NaN(规则 1)
- NaN + "Tecent": NaN to string -> "NaN", "NaN" + "Tecent" = "NaNTencent"(规则 2)
- "NaNTencent" + []: [] to primitive(string) -> "", NaNTencent"(规则 3)
- "NaNTencent" + null: null to string -> "null", "NaNTencent" + "null" = "NaNTencentnull"(规则 2) 9: "NaNTencentnull" + 9: 9 to string -> "9", "NaNTencentnull" + "9" = "NaNTencentnull9" 10: "NaNTencentnull9" + false: false to string -> "false", "NaNTencentnull9" + "false" = "NaNTencentnull9false"
3.6 算术运算符和一元运算符
算数运算符和一元运算符的类型转换规则很简单, 也就是将非number
类型的值转换为number类型, 类型转换规则参照 2.2节
🧡 结尾
如果这篇文章 对你的学习 有所 帮助,欢迎 点赞 👍 收藏 ⭐ 留言 📝 ,你的支持 是我 创作分享 的 动力!
学习过程中如果有疑问,可以私信与我交流~
参考资料
- 17道题彻底理解 JavaScript 中的类型转换 - 掘金
- 通过面试题研究JavaScript数据类型转换
- <你不知道的JavaScript_中卷>
- MDN