数据类型分类
JS的数据类型分为两大类,一类是原始类型(primitive type),一类是对象类型(object
type)。
原始类型
原始类型又称为基本类型,分为 Number , String , Boolean , Undefined , Null 几类。比较特殊的是, undefined 是 Undefined 类型中的唯一一个值;同样地, null 是 Null 类型中的唯一一个值。
ES6 引入了一个比较特殊的原始类型 Symbol ,用于表示一个独一无二的值。Symbol是原始类型,不是对象类型。因为Symbol 是没有构造函数 constructor 的,不能通过new Symbol() 获得实例。
但是获取 symbol 类型的值是通过调用Symbol 函数得到的。
1 | const symbol1 = Symbol('Tusi') |
Symbol 值是唯一的,所以下面的等式是不成立的。
1 | Symbol(1) === Symbol(1) // false |
对象类型
对象类型也叫引用类型,简单地理解呢,对象就是键值对 key:value 的集合。常见的对象类型有 Object , Array , Function , Date , RegExp 等。
JS还有全局对象。全局对象并不意味着它就是一种对象类型。比如JSON是一个全局对象,但是它不是一个对象类型。
对象可以 new 出来,所以对象类型都有构造函数, Object 类型对应的构造函数是Object() , Array 类型对应的构造函数是 Array()。
1 | var obj = new Object() // 不过我们⼀般也不会这么写⼀个普通对象 |
栈内存和堆内存
栈内存的优势是存取速度比堆内存快,考虑这一点可以优化代码性能。
栈内存
原始类型是按值访问的,其值存储在栈内存中,所占内存大小是已知的或是有范围的;
对基本类型变量的重新赋值,其本质上是进行压栈操作,写入新的值,并让变量指向一块栈顶元素。
1 | var a = 1; // 压栈,1成为栈顶元素,其值赋给变量a |
堆内存
对象类型是按引用访问的,通过指针访问对象。
指针是一个地址值,类似于基本类型,存储于栈内存中,是变量访问对象的中间媒介。
对象本身存储在堆内存中,其占用内存大小是可变的,未知的。
举例:
1 | var b = { name: 'Tusi' } |
运行这行代码,会在堆内存中开辟一段内存空间,存储对象 {name: ‘Tusi’} ,同时声明一个指针,其值为上述对象的内存地址,指针赋值给引用变量 b ,意味着 b 引用了上述对象。
对象可以新增或删除属性,所以说对象类型占用的内存大小一般是未知的。
1 | b.age = 18; // 对象新增了age属性 |
按引用访问是对引用变量进行对象操作,其本质上改变的是引用变量所指向的堆内存地址中的对象本身。
如果有两个或两个以上的引用变量指向同一个对象,那么对其中一个引用变量的对象操作,会影响指向该对象的其他引用变量。
1 | var b = { name: 'Tusi' }; // 创建对象,变量b指向该对象 |
考虑到对象操作的副作用,我们会在业务代码中经常使用深拷贝来规避这个问题。
数据类型的判断
typeof
typeof
操作符返回一个字符串,表示未经计算的操作数的类型。
数据类型 | 运算结果 |
---|---|
Undefined | “undefined” |
Null | “object” |
Boolean | “boolean” |
Number | “number” |
String | “string” |
Symbol | “symbol” |
Function | “function” |
其他对象 | “object” |
宿主对象(由JS环境提供,如Nodejs有global,浏览器有window) | 取决于具体实现 |
typeof null
的结果也是"object"
- 对象的种类很多,
typeof
得到的结果无法判断出数组,普通对象,其他特殊对象
instanceof
instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。
利用instanceof
,我们可以判断一个对象是不是某个构造函数的实例。那么结合typeof
,我们可以封装一个基本的判断数据类型的函数。
基本思想是:首先看typeof
是不是返回"object"
,如果不是,说明是普通数据类型,那么直接返回typeof
运算结果即可;如果是,则需要先把null
这个坑货摘出来,然后依次判断其他对象类型。
1 | function getType(val) { |
toString
Object.prototype.toString
。也可以判断ES6
引入的一些新的对象类型,比如Map
, Set
等。
1 | // 利用了Object.prototype.toString和正则表达式的捕获组 |
为什么普通的调用toString
不能判断数据类型,而Object.prototype.toString
可以呢?
因为Object
是基类,而各个派生类,如Date
, Array
等在继承Object
的时候,一般都重写(overwrite
)了toString
方法,用以表达自身业务,从而失去了判断类型的能力。
装箱和拆箱
把原始类型转换为对应的对象类型的操作称为装箱,反之是拆箱。
装箱
只有对象才可以拥有属性和方法,但是我们在使用一些基本类型数据的时候,却可以直接调用它们的一些属性或方法,这是怎么回事呢?
1 | var a = 1; |
其实在读取一些基本类型数据的属性或方法时,javascript
会创建临时对象(也称为“包装对象”),通过这个临时对象来读取属性或方法。以上代码等价于:
1 | var a = 1; |
临时对象是只读的,可以理解为它们在发生读操作后就销毁了,所以不能给它们定义新的属性,也不能修改它们现有的属性。
1 | var c = '123'; |
我们也可以显示地进行装箱操作,即通过String()
, Number()
, Boolean()
构造函数来显示地创建包装对象。
1 | var b = 'I love study'; |
拆箱
对象的拆箱操作是通过valueOf
和toString
完成的。
类型的转换
javascript
在某些场景会自动执行类型转换操作,而我们也会根据业务的需要进行数据类型的转换。类型的转换规则如下:
对象到原始值的转换
toString
toString()
是默认的对象到字符串的转换方法。
1 | var a = {}; |
但是很多类都自定义了toString()
方法,举例如下:
- Array:将数组元素用逗号拼接成字符串作为返回值。
1 | var a = [1, 2, 3]; |
- Function:返回一个字符串,字符串的内容是函数源代码。
- Date:返回一个日期时间字符串。
1 | var a = new Date(); |
- RegExp:返回表示正则表达式直接量的字符串。
1 | var a = /\d+/; |
valueOf
valueOf()
会默认地返回对象本身,包括Object
, Array
, Function
, RegExp
。
日期类Date
重写了valueOf()
方法,返回一个1970年1月1日以来的毫秒数。
1 | var a = new Date(); |
对象–>布尔值
从上表可见,对象(包括数组和函数)转换为布尔值都是true
。
对象–>字符串
对象转字符串的基本规则如下:
- 如果对象具有
toString()
方法,则调用这个方法。如果它返回字符串,则作为转换的结果;如果它返回其他原始值,则将原始值转为字符串,作为转换的结果。 - 如果对象没有
toString()
方法,或toString()
不返回原始值(不返回原始值这种情况好像没见过,一般是自定义类的toString()
方法吧),那么javascript
会调用valueOf()
方法。如果存在valueOf()
方法并且valueOf()
方法返回一个原始值,javascript
将这个值转换为字符串(如果这个原始值本身不是字符串),作为转换的结果。 - 否则,
javascript
无法从toString()
或valueOf()
获得一个原始值,会抛出异常。
对象–>数字
与对象转字符串的规则类似,只不过是优先调用valueOf()
。
- 如果对象具有
valueOf()
方法,且valueOf()
返回一个原始值,则javascript
将这个原始值转换为数字(如果原始值本身不是数字),作为转换结果。 - 否则,如果对象有
toString()
方法且返回一个原始值,javascript
将这个原始值转换为数字,作为转换结果。 - 否则,
javascript
将抛出一个类型错误异常。
显式转换
使用String()
, Number()
, Boolean()
函数强制转换类型。
1 | var a = 1; |
隐式转换
加法运算符+
因为加法运算符+
可以用于数字加法,也可以用于字符串连接,所以加法运算符的两个操作数可能是类型不一致的。
- 如果其中一个运算符是对象,则会遵循对象到原始值的转换规则,对于非日期对象来说,对象到原始值的转换基本上是对象到数字的转换,所以首先调用
valueOf()
,然而大部分对象的valueOf()
返回的值都是对象本身,不是一个原始值,所以最后也是调用toString()
去获得原始值。对于日期对象来说,会使用对象到字符串的转换,所以首先调用toString()
。
1 | 1 + {}; // "1[object Object]" |
- 在进行了对象到原始值的转换后,如果加法运算符
+
的其中一个操作数是字符串的话,就将另一个操作数也转换为字符串,然后进行字符串连接。
1 | var a = {} + false; // "[object Object]false" |
- 否则,两个操作数都将转换为数字(或者NaN),然后进行加法操作。
1 | var a = 1 + true; // 2 |
[] == ![]
[] == ![]
,其结果是true
。
首先,我们要知道运算符的优先级是这样的,一元运算符
!
的优先级高于关系运算符==
。所以,右侧的
![]
首先会执行,而逻辑非运算符!
会首先将其操作数转为布尔值,再进行求反。[]
转为布尔值是true
,所以![]
的结果是false
。此时的比较变成了[] == false
。根据比较规则,如果
==
的其中一个值是false
,则将其转换为数字0
,再与另一个操作数比较。此时的比较变成了[] == 0
。接着,再参考比较规则,如果一个值是对象,另一个值是数字或字符串,则将对象转为原始值,再进行比较。左侧的
[]
转为原始值是空字符串""
,所以此时的比较变成了"" == 0
。最后,如果一个值是数字,另一个是字符串,先将字符串转换为数字,再进行比较。空字符串会转为数字
0
,0
与0
自然是相等的。
也可以分析下为什么{} == !{}
的结果是false
了。