说起 JS 不得不先说,数据类型,每一种语言都有自己的数据类型,比如 Java 的数据类型,有 Int、String 等,它是强类型语言,但是 JS 是一种弱类型语言,只有一种命名变量的方式 var
或者 ES6 的新语法 let
和 const
。(这里主要说 var)
虽然只是用 var 来声明变量,但是声明的变量可以根据赋值来自动判断类型。这一章我们主要从数据类型的概括、检测方法、转换方式来学习 JS 的数据类型。
数据类型概括
数据类型分为 基础类型 和 引用类型
-
其中基础类型包括:
undefined
Null
Number
String
Boolean
Symbol
BigInt
-
引用类型:
Object
(其中Object又包括Array
RegExp
Date
Math
Function
)
为什么会有基础类型和引用类型?
这是因为它们两种类型存储在不同内存中,因此上面的数据类型分成两类来进行存储:
- 基础数据类型存储在栈内存中,被引用或拷贝时,会创建一个完全相等的变量;
- 引用类型存储在堆内存中,存储的是地址,多个引用可能指向同一个内存地址,从而数据也发生了共享。
举一个简单例子:
var a = {
name: '小白',
age: 6
}
var b = a;
console.log(a.name); // 小白
b.name = '小黑';
console.log(a.name); // 小黑
console.log(b.name); // 小黑
我们改变了 b.name
之后,a.name
也随着发生了改变,这是因为a是引用类型,声明的变量b直接等于a,这就相当于b直接引用a的内存地址,这样a和b的之间的数据产生了共享。
再来举一个复杂的例子(也是一道常考的面试题):
var a = {
name: '小白',
age: 3
}
var b = foo(a);
function foo (data) {
data.name = '小黑';
data = {
name: '小王',
age: 18
}
return data;
}
console.log(a); // {name: '小黑', age: 3}
console.log(b); // {name: '小王', age: 18}
在这个例子中,我们可以看到 我们将a
这个变量传到 foo
函数中,在这个函数中我们改变了它的name
属性值,在函数里面的 data.name
还是和 a.name
引用同一个内存地址,因此name
属性值被改变了。但是b
变量的值是等于一个函数的返回值,return 返回的是一个新的对象,开辟了一块新的内存地址,所以b的值为 {name: '小王', age: 18}
。
数据类型检测
数据类型检测是关于数据类型很重要的一环,也是面试过程中经常被问到的问题。
首先说一下经常用的的数据类型检测的方法:
- typeof 直接检测数据类型
- instanceof 判断对象的原型链
- Object.prototype.toString.call(obj)
第一种方法:typeof
typeof 可以精准的检测出基础数据类型,下面举一些例子
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object'
typeof [] // 'object'
typeof {} // 'object'
typeof console // 'object'
typeof console.log // 'function'
typeof Function // 'function'
typeof Array // 'function'
由上图的输出,我们可以看到 前 5 个基础数据类型,利用 typeof 都可以精准的检测出来,但是 null 作为一个基础数据类型,却被检测成 object,但是null本身并不是对象,这是 JS 的一个 Bug。 接下来检测 数组的字面量[]
,对象的字面量{}
,以及函数 Function
等都被检测成对象类型。因此我们可以得出,typeof 可以检测出基础类型数据(除 null 外),对于引用类型不能准确检测出来。
第二种方法: instanceof
我们通过 new
实例化构造函数,创建一个新的对象,这个新对象继承了构造函数的方法,通过原型链向上可以找到,而 instanceof
就可以判断出这个对象是不是由之前的构造函数创建出来的对象,这样就可以判断出新对象的数据类型。
var Car = function() {}
var car = new Car()
car instanceof Car // true
var a = new String('abc')
a instanceof String // true
var str = 'abc'
str instanceof String // false
由上面的代码可知,car
是通过实例化 Car
这个构造函数创建出来的对象,因此car
能够使用instanceof
向上找到自己的构造函数原型 Car
;通过 new String()
实例化出来的字符串,因此 a
的也在 String
的原型上;但是对于基础类型的检测,instanceof
是做不到的。
接下来我们来手写一个 instanceof 方法(面试中常遇到):
// 创建一个函数,传两个参数,item为要被检测的对象,type为类型
function myInstanceof(item, type) {
// 如果item不为对象,则不能检测,且不能检测 null
if(typeof item !== 'object' || item === null) return false;
// 拿到对象 item 的原型
var proto = Object.getPrototypeOf(item);
while(true) {
// 找到空则说明原型上没有,返回false
if(proto === null) return false;
// 找到相同的原型对象,返回true
else if(proto === type.prototype) return true;
// 原型链上找到该原型,停止循环
proto = Object.getPrototypeOf(proto);
}
}
myInstanceof(new String('123'), String); // true
myInstanceof(123, Number); // false
总结一下上面我的两种方法:
instanceof
可以判断出复杂的引用数据类型,但是不能判断出基础的数据类型;typeof
可以判断出基础的数据类型,但是不能判断出引用数据类型(null 除外,引用类型的 function 可以检测出来)。
第三种检测方法:Object.prototype.toString.call(obj)
toString()
是 Object
原型上的方法,调用方法,返回字符串 "[object Xxx]"
,Xxx就是对象的类型。对于 Object
对象,直接调用 toString()
方法,就会返回 "[object Object]"
,但是对于其他类型,需要通过 call()
方法来调用,才能返回正确的信息。
举一些例子:
Object.prototype.toString({}) // "[object Object]"
Object.prototype.toString.call(123) // "[object Number]"
Object.prototype.toString.call('') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(function(){}) // "[object Function]"
Object.prototype.toString.call(null) //"[object Null]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(/123/g) //"[object RegExp]"
Object.prototype.toString.call(new Date()) //"[object Date]"
Object.prototype.toString.call([]) //"[object Array]"
Object.prototype.toString.call(document) //"[object HTMLDocument]"
Object.prototype.toString.call(window) //"[object Window]"
注意:typeof
检测出来的类型,首字母是小写的,而使用 toString
方法检测出来的对象类型 'Xxx'
,首字母是大写。
如何取出检测出来的类型呢?
Object.prototype.toString.call({}).replace(/^\[object (\S+)\]$/, '$1')
直接返回出 'Object'
那么我们基于这个条件,来写一个通用的判断方法:
function getType(obj) {
let type = typeof obj;
if(type !== 'object') {
return type;
}
let otherType = Object.prototype.toString.call(obj).replace(/\[object (\S+)\]$/, '$1');
return otherType.toLowerCase();
}
数据类型转换
在日常开发中会经常遇到数据的类型转换,类型转换分为:
- 强制类型转换
- 隐式类型转换
强制类型转换
强制类型转换包括:Number() String() Boolean() parseInt() parseFloat() toString()
Number() 强制类型转换
- 如果是布尔值,
true
和false
分别被转换为1
和0
; - 如果是数字,返回自身;
- 如果是
null
,返回0
; - 如果是
undefined
,返回NaN
; - 如果是字符串,空字符串,将其转换为 0;字符串中只包含数字(或者是 0X / 0x 开头的十六进制数字字符串,允许包含正负号),则将其转换为十进制;如果字符串中包含有效的浮点格式,将其转换为浮点数值;如果不是以上格式的字符串,均返回
NaN
; - 如果是
Symbol
,抛出错误; - 如果是对象,并且部署了
[Symbol.toPrimitive]
,那么调用此方法,否则调用对象的 valueOf() 方法,然后依据前面的规则转换返回的值;如果转换的结果是 NaN ,则调用对象的 toString() 方法,再次依照前面的顺序转换返回对应的值。
通过一段代码来解释上面的规则:
Number(true); // 1
Number(false); // 0
Number('0111'); //111
Number(null); //0
Number(''); //0
Number('sos'); //NaN
Number(-0X11); //-17
Number('0X11') //17
Boolean 强制转换规则
除了 undefined、 null、 false、 ' '、 0、 NaN 转换出来是 false,其他都是 true(也包括空数组,空对象等)。
代码:
Boolean(0) //false
Boolean(null) //false
Boolean(undefined) //false
Boolean(NaN) //false
Boolean(1) //true
Boolean(13) //true
Boolean('12') //true
上面两种类型转换是比较重要的,其他的就不说了。
隐式类型转换
隐式类型转换,一般出现在,逻辑运算符 (&&、 ||、 !)、运算符 (+、-、*、/)、关系操作符 (>、 <、 <= 、>=)、相等运算符 (==) 或者 if/while 条件的操作,如果遇到两个数据类型不一样的情况,都会出现隐式类型转换。
'==' 的隐式类型转换规则
- 如果类型相同,无须进行类型转换;
- 如果其中一个操作值是
null
或者undefined
,那么另一个操作符必须为null
或者undefined
,才会返回true
,否则都返回false
;(面试常考) - 如果其中一个是
Symbol
类型,那么返回false
; - 两个操作值如果都为
string
和number
类型,那么就会将字符串转换为number
; - 如果一个操作值是
boolean
,那么转换成number
; object
转为原始类型再进行判断,调用object
的valueOf/toString
方法进行转换。
案例:
null == undefined // true
null == 0 // false
'' == null // false
'' == 0 // true
'123' == 123 // true
0 == false // true
1 == true // true
'+' 的隐式类型转换规则
'+' 号操作符,不仅可以用作数字相加,还可以用作字符串拼接。仅当 '+' 号两边都是数字时,进行的是加法运算;如果两边都是字符串,则直接拼接,无须进行隐式类型转换。
除了上述比较常规的情况外,还有一些特殊的规则:
- 如果其中有一个是字符串,另外一个是
undefined
、null
或布尔型,则调用toString()
方法进行字符串拼接;如果是纯对象、数组、正则等,则默认调用对象的转换方法会存在优先级,然后再进行拼接; - 如果其中有一个是数字,另外一个是
undefined
、null
、布尔型或数字,则会将其转换成数字进行加法运算,对象的情况还是参考上一条规则; - 如果其中一个是字符串、一个是数字,则按照字符串规则进行拼接。
代码案例:
1 + 2 // 3
'1' + '2' // '12'
// 下面为特殊情况
'1' + 2 // '12' 字符串拼接
'1' + undefined // "1undefined" undefined转换字符串
'1' + null // "1null" null转换字符串
'1' + true // "1true" true转换字符串
'1' + 1n // '11' 比较特殊字符串和BigInt相加,BigInt转换为字符串
1 + undefined // NaN undefined转换数字相加NaN
1 + null // 1 null转换为0
1 + true // 2 true转换为1,二者相加为2
1 + 1n // 报错 不能把BigInt和Number类型直接混合相加
Object 的转换规则
对象转换的规则,其实就是向基础类型转,如下:
- 如果部署了
Symbol.toPrimitive
方法,优先调用再返回; - 调用
valueOf()
,如果转换为基础类型,则返回; - 调用
toString()
,如果转换为基础类型,则返回; - 如果都没有返回基础类型,会报错。
var obj = {
value: 1,
valueOf() {
return 2;
},
toString() {
return '3'
},
[Symbol.toPrimitive]() {
return 4
}
}
console.log(obj); // 输出4
// 因为有Symbol.toPrimitive,就优先执行这个;如果Symbol.toPrimitive这段代码删掉,则执行valueOf打印结果为2;如果valueOf也去掉,则调用toString返回'3'
10 + {}
// "10[object Object]",{}会默认调用valueOf是{},不是基础类型继续转换,调用toString,返回结果"[object Object]",于是和10进行'+'运算,按照字符串拼接规则来
[1,2,undefined,4,5] + 10
// "1,2,,4,510",[1,2,undefined,4,5]会默认先调用valueOf结果还是这个数组,不是基础数据类型继续转换,也还是调用toString,返回"1,2,,4,5",然后再和10进行运算
总结
- JS 的数据类型是,是必须要掌握的是学习类型检测和类型转换的基础
- 数据类型检测方法主要有三种:typeof 、instanceof 以及 Object.prototype.toString.call()
- 数据类型转换:有强制类型转换和隐式类型转换