全面搞定 JavaScript 的数据类型 以及 判断方法 & 前端必备

353 阅读10分钟

说起 JS 不得不先说,数据类型,每一种语言都有自己的数据类型,比如 Java 的数据类型,有 Int、String 等,它是强类型语言,但是 JS 是一种弱类型语言,只有一种命名变量的方式 var 或者 ES6 的新语法 letconst 。(这里主要说 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}

数据类型检测

数据类型检测是关于数据类型很重要的一环,也是面试过程中经常被问到的问题。

首先说一下经常用的的数据类型检测的方法:

  1. typeof 直接检测数据类型
  2. instanceof 判断对象的原型链
  3. 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() 强制类型转换

  • 如果是布尔值,truefalse 分别被转换为 10
  • 如果是数字,返回自身;
  • 如果是 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
  • 两个操作值如果都为 stringnumber 类型,那么就会将字符串转换为 number
  • 如果一个操作值是 boolean,那么转换成 number
  • object 转为原始类型再进行判断,调用 objectvalueOf/toString 方法进行转换。

案例:

null == undefined       // true 
null == 0               // false 
'' == null              // false 
'' == 0                 // true
'123' == 123            // true 
0 == false              // true 
1 == true               // true 

'+' 的隐式类型转换规则

'+' 号操作符,不仅可以用作数字相加,还可以用作字符串拼接。仅当 '+' 号两边都是数字时,进行的是加法运算;如果两边都是字符串,则直接拼接,无须进行隐式类型转换。

除了上述比较常规的情况外,还有一些特殊的规则:

  • 如果其中有一个是字符串,另外一个是 undefinednull 或布尔型,则调用 toString() 方法进行字符串拼接;如果是纯对象、数组、正则等,则默认调用对象的转换方法会存在优先级,然后再进行拼接;
  • 如果其中有一个是数字,另外一个是 undefinednull、布尔型或数字,则会将其转换成数字进行加法运算,对象的情况还是参考上一条规则;
  • 如果其中一个是字符串、一个是数字,则按照字符串规则进行拼接。

代码案例:

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()
  • 数据类型转换:有强制类型转换和隐式类型转换