今日前端小知识——判断数据类型的几种方法


JavaScript 是一种弱类型或者说动态语言,在使用一个变量时无需提前声明变量的类型,在程序运行过程中,类型会被自动确定,这意味着可以使用同一个变量保存不同类型的数据。
JavaScript 中数据类型主要分为两种:基本数据类型和引用数据类型。基本数据类型包括 Number、Boolean、String、undefined、null、Symbol、BigInt,引用数据类型包括 Object、Function、Array、Date、Map、Set 等多种对象。实际开发中拿到一个数据可能需要先对数据的类型进行判断,这里总结了几种常用的方法以及对应的优缺点。

typeof

typeof 操作符返回一个字符串,表示未经计算的操作数的类型。语法如下。

typeof operand / typeof(operand)

typeof 能判断出以下 8 种类型:Number、Boolean、String、undefined、Symbol、BigInt、Object、Function。需要注意的几点:

  • typeof null === ‘object’
  • typeof NaN === ‘number’

在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),null 的类型标签是 0,typeof null 也因此返回 “object”。

typeof(1);		// number
typeof(1.3);	// number
typeof(NaN);	// number

typeof(true);	// boolean

typeof('');	    // string
typeof('hi');	// string
typeof(`hhh`);	// string

// 已声明但未赋值的变量
let one;
typeof(one);	// undefined
// 未声明的变量
typeof(none);	// undefined
typeof(undefined);	// undefined

typeof(13n);	// bigint

typeof(Symbol(13));  	// symbol

let obj = {
  name: 'ostuthere'
}
typeof(obj);	// object

let nowTime = new Date();
typeof(nowTime);	// object

let arr = [1, 2, 3];
typeof(arr);	// object

typeof(null);	// object

let func1 = function(){
  console.log('hello');
}
function func2(){
  console.log('hello');
}
typeof(func1);	// function
typeof(func2);	// function

优点:除了 null,其它基本数据类型都能准确地判断
缺点:function 之外的其它引用数据类型、基础数据类型 null 以及使用构造函数创建的 Number 、String、Boolean 等都被判断为 object

instanceof

instanceof 运算符可以判断一个对象(object)是否为某个构造函数(constructor)的实例,如果是就返回 true,否则返回 false。原理是判断构造函数的 prototype 属性是否出现在某个实例对象的原型链上,一般用来判断引用数据类型。语法如下。

object instanceof constructor

var arr1 = new Array(2);
arr1 instanceof Array;	// true
arr1 instanceof Object;	// true
var arr2 = [];
arr2 instanceof Array;	// true

var str1 = new String('123');
str1 instanceof String;	// true
str1 instanceof Object;	// true
var str2 = '1234';
// str2 是基本数据类型,不是 String 构造函数的实例对象,不存在原型链,所以返回 false
str2 instanceof String;	// false

let num1 = new Number('123');
num1 instanceof Number;	// true
num1 instanceof Object;	// true
let num2 = 13;
num2 instanceof Number;	// false

function A(name, age){
  this.name = name;
  this.age = age;
}
let p1 = new A('ostuthere', 18);
p1 instanceof A;		// true
p1 instanceof Object;	// true

p1 instanceof A 返回 true 并不代表一直都会是 true,因为 A.prototype 有可能被改变,改变后的 prototype 不一定会出现在 p1 的原型链上。

// 改变构造函数的原型之后,原来创建的实例会被判断为 false
A.prototype = {
  sayHi: function(){
    console.log('hi');
  }
}
p1 instanceof A;	// false

function B(name, age, gender){
  this.name = name;
  this.age = age;
  this.gender = gender;
}
p1 instanceof B;	// false
B.prototype = new A();	//	继承
let p2 = new B('Bob', 20, 'male');
p2 instanceof B;	// true
// A.prototype 出现在了 p2 的原型链上,所以返回 true
p2 instanceof A;	// true

优点:能够检测出引用数据类型
缺点:无法判断基本数据类型;由于构造函数的 prototype 能够更改,可能导致判断失误;不能跨 iframe 或 window(即不同的全局环境)

constructor

在 JavaScript 中,每个具有原型的对象都会自动获得 constructor 属性,它指向构造函数。除了 arguments、Enumerator、Error、Global、Math、RegExp、Regular Expression 等一些特殊对象外,其他所有的 JavaScript 内置对象都具备 constructor 属性,例如:Array、Boolean、Date、Function、Number、Object、String 等,注意基本数据类型(不包括 null 和 undefined)的 constructor 属性只读。由于 null 和 undefined 是无效对象,没有 constructor 属性,无法通过该方法判断。

let obj1 = {};
obj1.constructor === Object; // true
let obj2 = new Object;
obj2.constructor === Object; // true

let arr1 = [];
arr1.constructor === Array; // true
let arr2 = new Array;
arr2.constructor === Array // true

let num1 = new Number(13);
num1.constructor === Number; // true
let num2 = 13;
num2.constructor === Number; // true
num2.constructor;	// ƒ Number() { [native code] }

除了基本数据类型的 constructor 只读之外,对象的 constructor 是可以被更改的,如下

function typeFun(){}
num1.constructor;
// 将通过 Number 构造函数创建的实例对象 num1 的 constructor 指向自定义函数 typeFun
num1.constructor = typeFun;
num1.constructor === Number; // false
num1.constructor === typeFun;	// true

优点:能够检测出引用数据类型,还可以检测出 undefined 和 null 之外的基本数据类型
缺点:由于 constructor 所指向的构造函数是可以被修改的,可能导致判断失误;不能跨 iframe 或 window(即不同的全局环境)

Object.prototype.isPrototypeOf()

isPrototypeOf() 方法用于测试一个对象(prototypeObj)是否存在于另一个对象(object)的原型链上,若是则返回 true,否则返回 false。语法如下。

prototypeObj.isPrototypeOf(object)

isPrototypeOf() 与 instanceof 运算符不同。在表达式 "object instanceof Function"中,object 的原型链是针对 Function.prototype 进行检查的,而不是针对 Function 本身。

function funA() {}
function funB() {}
function funC() {}

funB.prototype = Object.create(funA.prototype);
funC.prototype = Object.create(funB.prototype);

let obj1 = new funC();

funC.prototype.isPrototypeOf(obj1); // true
funB.prototype.isPrototypeOf(obj1); // true
funA.prototype.isPrototypeOf(obj1); // true
Object.prototype.isPrototypeOf(obj1); // true

与 instanceof 类似,不能检测基本数据类型,且由于 prototype 能被修改导致检测结果不一定真实。

Object.prototype.toString.call()(最推荐使用)

Object.prototype.toString() 方法默认返回"[object type]",其中 type 是调用者的数据类型。从原型链的角度来说,所有对象最终都会指向 Object 对象,依据 JS 的变量查找规则,所有对象都能访问到 Object 的 toString() 方法,所以我们可以通过 toString() 来获取每个对象的类型。但实际上,不少对象都改写了 toString 方法。

BigInt(13).toString();	// '13'
let num = new Number(1)
num.toString();	// '1'
let arr = new Array(1,2,3)
arr.toString();	// '1,2,3'

为了保证每个对象都能通过 Object.prototype.toString() 方法来检测类型,我们需要通过 Object.prototype.toString.call() 或者 Object.prototype.toString.apply() 的形式来调用,第一个参数是要检查的对象。

Object.prototype.toString.call(1);	// [object Number]
Object.prototype.toString.call(true);	// [object Boolean]
Object.prototype.toString.call('13');	// [object String]
Object.prototype.toString.call(undefined);	// [object Undefined]
Object.prototype.toString.call(null);	// [object Null]
Object.prototype.toString.call(Symbol(13));	// [object Symbol]
Object.prototype.toString.call(13n);	// [object BigInt]
Object.prototype.toString.call([1,2,3]);	// [object Array]
Object.prototype.toString.call(new Function());	// [object Function]
Object.prototype.toString.call(new Date());	// [object Date]
Object.prototype.toString.call(new RegExp());	// [object RegExp]
Object.prototype.toString.call(new Error());	// [object Error]
Object.prototype.toString.call(window) ;	// [object global] window是全局对象global的引用

优点:能够检测出所有的数据类型
缺点:在 IE6 之前,这两者的类型都被判定为 Object(应该没人用这么旧的浏览器了吧)

补充知识点:instanceof 与 isPrototypeOf() 的区别

A instanceof B:A是对象,B是构造函数。判断构造函数 B 的原型是否在对象 A 的原型链上。
A.isPrototypeOf(B):A 与 B 都是对象。判断对象 A 是否出现在对象 B 的原型链上。
接下来手动实现 instanceof 和 isPrototypeOf() 并进行验证,感受一下两者的区别。

// 实现 instanceof
function myInstanceof(obj, _constructor){
  // 判断 obj 是否为基本数据类型
  // 判断 obj.__proto__ === _constructor.prototype
  let basicType = ['number', 'boolean', 'string', 'undefined', 'symbol', 'bigint'];
  let typeOfObj = typeof(obj);
  if (typeOfObj === null || basicType.includes(typeOfObj)) {
    return false;
  }
  let L = obj.__proto__;
  let R = _constructor.prototype;
  while (L) {
    if (L === R) {
      return true;
    } else if (L && L.__proto__) {
      L = L.__proto__;
    } else {
      return false;
    }
  }
}

console.log(myInstanceof([1, 2, 3], Array));	// true
console.log(myInstanceof(13, Number));	// false

// 实现 isPrototypeOf()
Object.prototype.myIsPrototypeOf = function (obj) {
  // 判断 obj 是否为基本数据类型
  // 判断 obj.__proto__ === this (当前调用对象)
  let basicType = ['number', 'boolean', 'string', 'undefined', 'symbol', 'bigint'];
  let typeOfObj = typeof(obj);
  if (typeOfObj === null || basicType.includes(typeOfObj)) {
    return false;
  }
  let L = obj.__proto__;
  while (L) {
    if (L === this) {
      return true;
    } else if (L && L.__proto__) {
      L = L.__proto__;
    } else {
      return false;
    }
  }
}

let p = {
  msg: 'hello'
}
let o = Object.create(p);
console.log(p.myIsPrototypeOf(o));	// true
console.log(Object.prototype.myIsPrototypeOf(o));	// true
console.log(Number.prototype.myIsPrototypeOf(1));	// false

通过上面的代码我们可以发现,instanceof 和 isPrototypeOf() 都会查找原型链,看目标对象是否存在于指定对象的原型链上。但前者查找的目标对象是构造函数的 prototype,后者所查找的目标对象就是函数调用者本身。