这是深入理解 JavaScript 语法合集的第三篇,其余文章见文末。
前言
前面一篇内容 我们讲解了基本数据类型,接着讨论了它们是如何进行存储的。在此同时,我们就 原始数据类型
和 对象类型
的区别进行了分析。那么这一篇我们继续讨论,这些数据类型之间是如何进行转换的?又还有哪些其它对象类型?JavaScript 中的内置对象有哪些?装箱和拆箱又是如何触发的?我们又该怎么判断变量的数据类型呢?
JavaScript 的数据类型
一、JavaScript 类型转换
在介绍 JavaScript 类型转换之前,我们先来介绍一些前置知识吧。
(1)还有哪些其它对象类型?
在 前一篇 文章中讲到,最新的 ECMAScript 中定义了八种数据类型。总的来说可以分为两种类型,一种是原始数据类型,另一种是对象类型,即 Object
。但实际上,Object
类型是对象类型的统称。它不仅仅包含 Object
类型,它还包含一些特殊的对象类型:Array
、Date
、RegExp
、Function
等类型。
虽然这些特殊类型生成的对象并不是由 Object
直接构造的,但它们的原型链终点都是 Object
。
(2)什么是基本包装类型?
基本包装类型也是一种特殊的对象类型。为了便于原始数据类型的操作,基本包装类型为原始数据类型提供了一套转换机制。基本包装类型和其它对象类型的区别在于生存期不同,前者在代码执行后就会销毁实例。
基本包装类型包括:String
、Number
、Boolean
。
我们来看看基本包装类型的生存周期:1
2
3let str = "car";
str.name = "HongQi";
console.log(str.name); // undefined
出乎意料的是,最后输出的值是 undefined
。我们把目光放到第二行,我们为该变量定义了一个属性 name
,设置为 HongQi
。我们知道,设置属性是对象的事情,原始数据类型是没有的。这说明,当我们为其设置属性时,String
类型(原始数据类型中的 String
)的变量被自动 装箱
成了包装类型的对象,而由于其在代码执行后被销毁了,所以我们在第三行访问变量时其值变为 undefined
。
注:Number
、Boolean
、String
三种类型也可以通过 new
关键字创建对应的基本包装类型实例。
(3)装箱、拆箱
刚刚稍微提到了,装箱
就是将原始数据类型转换成包装类型。而拆箱则相反,将包装类型反过来转换成原始数据类型。
先考虑一下,为什么都有了原始数据类型了还要有基本包装类型呢?
我们知道,原始数据类型本身并不能扩展属性和方法。这时倘若我们需要操作原始数据类型的变量,那我们就不能使用那些常用方法。这将让操作变的非常不方便。
1. 装箱:
假如现在我们要操作一个字符串,希望拿到它的其中一部分。我们可能会这样操作:1
2
3
4let str = "MyBooks";
let partOfStr = str.substring(2);
console.log(partOfStr); // Books
第二行原始类型 str
调用了 substring()
方法,然后将操作后的值给了 partOfStr
。这实际上发生了几个过程:
- 创建一个
String
的包装类型实例 - 在实例上调用
substring
方法 - 返回操作后的值同时销毁实例
也就是说,当我们使用原始数据类型调用方法时,就会自动进行装箱操作。类似的,我们对原始数据类型 Number
、Boolean
的变量使用相应的方法也会自动的进行对应的装箱操作。
2. 拆箱:
从对象类型到原始数据类型的转换就是拆箱过程。在拆箱的过程中,会遵循 ECMAScript
规范规定的 toPrimitive
原则:ToPrimitive(input[, PreferredType])
。
该原则将输入的 input
从对象类型转换为原始数据类型(non-Object type)。PreferredType
是可选参数,可以是 Number
或 String
类型, 只是一个转换标志。转化后的结果并不一定是这个参数所设置的类型。但是它的转换结果一定是一个原始值(或者报错), 也就是说设置 Number
时也可能转换为 String
, 设置为 String
时 也可能转化为 Number
。
根据转换算法,我们可以得出:
- 当
PreferredType
未设置时,则 input
是Date
类型时,PreferredType = String
;
- 否则,
PreferredType = Number
;
- 否则,
PreferredType = Number
:input
是原始值,返回原始值;
input
不是原始值,调用input.valueOf()
;如果返回值是原始值,则返回原始值结果;
- 若步骤 2 的结果不是原始值,调用
input.toString()
。如果返回值是原始值,则返回原始值结果;
- 若步骤 2 的结果不是原始值,调用
- 如果 3 的结果不是原始值,抛出
TypeError
异常。
- 如果 3 的结果不是原始值,抛出
PreferredType = String
:input
是原始值,返回原始值;
input
不是原始值,调用input.toString()
;如果返回值是原始值,则返回原始值结果。
- 如果 2 的结果不是原始值,调用
input.valueOf()
,如果返回值是原始值,则返回原始值结果;
- 如果 2 的结果不是原始值,调用
- 如果 3 的结果不是原始值,抛出
TypeError
异常。
- 如果 3 的结果不是原始值,抛出
总结就是:
- 当
PreferredType
未被设置时,若input
为Date
,则将PreferredType
设置为String
。否则设置为Number
- 当
PreferredType
为Number
时,先调用input
的valueOf
,若结果为原始值则返回;否则调用toString
,若结果为原始值则返回;否则返回TypeError
; - 当
PreferredType
为String
时,先调用toString
,若结果为原始值则返回;否则调用valueOf
,若结果为原始值则返回;否则返回TypeError
;
3. valueOf() 方法和 toString() 方法
这两个方法是 Object.prototype
的方法,也就是所有的对象都有这两个方法。
valueOf()
:原则是,能转化为原始值就转化为原始值,不能转化为原始值的返回this
,也就是对象本身;Date
对象转化为毫秒级数值1
2
3
4
5
6
7
8
9
10
11Number('123').valueOf() // 123
String('123fs').valueOf() // '123fs'
Boolean(true).valueOf() // true
new Date().valueOf() // 1530706938289
var arr = ['1', '2'];
arr.valueOf() // [ '1', '2' ]
var obj = {};
obj.valueOf() // {}toString()
:将对象转成字符串
除了程序中的自动装箱和自动拆箱,我们还可以手动进行拆箱和装箱操作。直接调用包装类型的 valueOf
或 toString
:1
2
3
4let num = new Number("123");
console.log(typeof num.valueOf()); // number
console.log(typeof num.toString()); // string
(4)强制类型转换
前置内容讲完了,现在我们来讲类型转换。
在 《你不知道的 JavaScript》中卷中这样写道:
将值从一种类型转换成另一种类型成为类型转换,这是显示的情况;隐式的情况称为强制类型转换。我们也可以这样来区分:类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在动态类型语言的运行时。然而,在 JavaScript 中通常将它们统称为强制类型转换。
这里我们就用 “隐式强制类型转换” 和 “显示强制类型转换” 来区分。显示强制类型转换这里就不再赘述了,主要来说说隐式类型转换。
类型转换主要包括:原始数据类型之间的相互转换、原始数据与对象类型的相互转换。后者我们刚刚已经在 “装箱” “拆箱” 中提到:原始数据类型是如何转换成相应的对象类型,对象类型转换成原始数据类型又该遵循怎样的原则。接下来介绍前者。
在转换的过程中,我们会使用到规范第 9 节中定义的 “抽象操作”:ToString
,ToNumber
,ToBoolean
。
ToNumber规则:1
2
3
4
5
6undefined NaN
null +0
boolean true: 1/ false: +0
number number
string '123': 123 / 'qwer': NaN
object ToPrimitive(input, Number).ToNumber()
ToString规则:1
2
3
4
5
6undefined "undefined"
null "null"
boolean true: "true"/ false: "false"
number "number"
string string
object ToPrimitive(input, String).ToString()
ToBoolean规则:1
2
3
4
5
6
7
8
9
10// 以下值在进行强制类型转换时均被转换为假值,即 false
undefined
null
false
+0、-0、NaN
""
// 其余所有值在强制转换的过程中均为真值,即 true
{}
[]
涉及到隐式转换的运算符最多的是 -*/
和 +
,==
等运算。其中,当 -*/
对非 Number
类型进行操作时,会将非 Number
类型转换为 Number
类型,即符合 ToNumber
规则。1
2
3
41 - undefined // NaN
1 - null // 1
2 * true // 2
123 / '123' // 1
这里需要注意的是符号 +
。当执行该操作时满足以下规则:
- 当其中一个操作数为字符串(或者能通过
ToPrimitive
操作得到字符串),则执行字符串拼接操作。非字符串的操作数将优先转换为字符串类型。 - 否则,执行数字加法。
1
2
3
4
5123 + '123' // 123123
123 + {} // 123[object Object]
123 + null // 123
123 + true // 124
而对于运算符 ==
,在它执行的过程中会对两侧的操作数进行隐式转换(如果值为非 Boolean
的话),即遵循 ToBoolean
原则。
(5)总结
上面讲解的内容中,虽然没有将所有类型转换一一列出来,但是我们把最主要的规则做了介绍,了解这一点是至关重要的。
我们先介绍了除 Object
之外的对象类型以及部分原始数据的包装类型。接着我们了解了原始数据类型是如何通过隐式的方式转换成包装类型的以及对象类型转换成原始数据类型的详细过程。最后还介绍了对象类型与原始数据类型、原始数据类型之间的转换规则。
二、如何判断变量的数据类型?
(1)typeof
是如何用的?
我们经常听到可以使用 typeof
操作符来判断变量的类型,但是好像又听到有些情况它又分辨不清楚,这到底是怎么回事呢?现在,先来回忆以下我们都介绍了哪些类型:
原始数据类型:Undefined
、Null
、Boolean
、Number
、String
、Symbol
、BigInt
。
对象类型:Object
、Array
、Date
、RegExp
、Function
。
在对象类型中,其中Object
、Array
、Date
、RegExp
可以统称为对象类型系统。
我们先来做个实验:1
2
3
4
5
6
7
8
9
10
11
12
13
14console.log( typeof undefined ) // undefined
console.log( typeof true ) // boolean
console.log( typeof 123 ) // number
console.log( typeof '千竹' ) // string
console.log( typeof Symbol() ) // symbol
console.log( typeof null ) // object
console.log( typeof {} ) // object
console.log( typeof [] ) // object
console.log( typeof new Date() ) // object
console.log( (typeof /^\d+$/) ) // object
console.log( (typeof function(){} )) // function
观察一下,对于原始数据类型,除了 null
,使用 typeof
运算符都能准确的得知变量的类型。而对于对象类型,我们除了 Function
类型,其他的(对象类型系统)均无法准确的检测出其类型。
对于
null
,它检测出来的类型为object
。这种现象在 JavaScript 诞生以来就是这样。在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签为 0 。由于null
代表的是空指针(大对数平台下值为 0x00)。因此,null 的类型标签也是 0。这也就是为什么typeof null
会返回object
的原因。
那么对于对象系统中的类型我们又该如何判断呢?
(2)instanceof
instanceof
操作符的语法是:1
object instanceof constructor
它可以帮助我们判断对象系统中变量的具体类型:
1 | console.log( [] instanceof Array); // true |
但是,如果你这样写,它也会返回 true
:1
console.log( [] instanceof Object);
这是因为 instanceof
的作用是用来检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。很显然,它检测出来的结果并不准确。
(3)toString
我们前面讲过,toString
类型是 Object.prototype
上面的方法,也就是所有对象类型都具有的方法。在该方法未被覆盖的情况下,当我们调用该方法,它就会返回 "[object, type]"
,其中的 type
就是对象的类型。
不过可能会让你失望,大部分引用类型都重写了 toString
方法。那我们应该怎么办呢?
既然 toString
方法被重写了,那我们就回到原型链的顶端来检测:
1 | console.log(Object.prototype.toString.call(undefined)); // [object Undefined] |
(4)总结
我们通过两篇文章的篇幅大致的讲解了一下 JavaScript 中的变量及类型。主要的路线为: JavaScript 有哪些数据类型?这些类型的变量又是怎么存储的呢?这些类型的变量是如何相互转换的?最后,我们又应该如何去检测这些变量呢?
关于数据类型及变量的知识远不止这些,今天先开个头,今后我们慢慢探索。
这是深入理解 JavaScript 语法合集的其中一篇,其它合集:
1. 深入理解 JavaScript —— 从设计一门语言讲起
2. 你真的掌握了 JavaScript 变量和类型嘛?(上)
4. JavaScript 的原型和原型链
5. JavaScript 中的对象
参考文章:
1. JavaScript - 数据类型及隐式转换-2017-06-05 by 面向信仰
2. 你真的掌握变量和类型了吗-2019-05-28 by ConardLi
3.《你不知道的 JavaScript 》中卷 by Kyle Simpson
4. Javascript原始值-2016-12-11 by Web技术研究室 lyz810
5. MDN Web docs——instanceof-2019-09-27 by MDN contributors
有问题?发送 issues 给我~