判断一个数据的类型,比较常用的有下面几种方式:

  • typeof
  • instanceof
  • Object.prototype.toString.call(xxx)

typeof

判断一个数据的类型,用得最多的就是 typeof 操作符, 但是使用 typeof 常常会遇到以下问题:

  • 无法判断 null
  • 无法判断除了 function 之外的引用类型。
// 可以判断除了 null 之外的基础类型。
console.log(typeof true); // 'boolean'
console.log(typeof 100); // 'number'
console.log(typeof "abc"); // 'string'
console.log(typeof 100n); // 'bigint'
console.log(typeof undefined); // 'undefined'
console.log(typeof Symbol("a")); // 'symbol'

// 无法判断 null。
console.log(typeof null); // 输出 'object',原因在文章末尾解释。

// 无法判断除了 function 之外的引用类型。
console.log(typeof []); // 'object'
console.log(typeof {}); // 'object'

instanceof

typeof 无法精确地判断引用类型,这时,可以使用 instanceof 运算符,如下代码所示:

console.log([] instanceof Array); // true

const obj = {};
console.log(obj instanceof Object); // true

const fn = function () {};
console.log(fn instanceof Function); // true

const date = new Date();
console.log(date instanceof Date); // true

const re = /abc/;
console.log(re instanceof RegExp); // true

但是 instanceof 运算符一定要是判断对象实例的时候才是正确的,也就是说,它不能判断原始类型,如下代码所示:

const str1 = "qwe";
const str2 = new String("qwe");

console.log(str1 instanceof String); // false,无法判断原始类型。
console.log(str2 instanceof String); // true

typeof 可以判断原始类型,instanceof 可以判断引用类型,但是当数据为null是,它们无法判断,我们可以直接判断变量全等于 null,如下代码所示:

function getType(target) {
  // ...
  if (target === null) {
    return "null";
  }
  // ...
}

现在,判断原始类型和引用类型的思路都有了,接下来就是动手写代码的事,但是真的去写就会发现,使用 instanceof 操作符来判断类型返回的是 true 或者 false,写起来会非常麻烦。

其实, instanceof 运算符本来是用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上的,只是刚好可以用来判断类型而已,所以在这里才会讨论它,实际上用它来判断类型代码写起来不是很方便。

这时,Object.prototype.toString 出场了,实际项目中要封装判断类型的工具函数一般都是用的它。

Object.prototype.toString.call(xxx)

调用 Object.prototype.toString 方法,会统一返回格式为 [object Xxx] 的字符串,用来表示该对象(原始类型是包装对象)的类型。

需要注意的是,在调用该方法时,需要加上 call 方法(原因后文解释),如下代码所示:

// 引用类型
console.log(Object.prototype.toString.call({})); // '[object Object]'
console.log(Object.prototype.toString.call(function () {})); // "[object Function]'
console.log(Object.prototype.toString.call(/123/g)); // '[object RegExp]'
console.log(Object.prototype.toString.call(new Date())); // '[object Date]'
console.log(Object.prototype.toString.call(new Error())); // '[object Error]'
console.log(Object.prototype.toString.call([])); // '[object Array]'
console.log(Object.prototype.toString.call(new Map())); // '[object Map]'
console.log(Object.prototype.toString.call(new Set())); // '[object Set]'
console.log(Object.prototype.toString.call(new WeakMap())); // '[object WeakMap]'
console.log(Object.prototype.toString.call(new WeakSet())); // '[object WeakSet]'

// 原始类型
console.log(Object.prototype.toString.call(1)); // '[object Number]'
console.log(Object.prototype.toString.call("abc")); // '[object String]'
console.log(Object.prototype.toString.call(true)); // '[object Boolean]'
console.log(Object.prototype.toString.call(1n)); // '[object BigInt]'
console.log(Object.prototype.toString.call(null)); // '[object Null]'
console.log(Object.prototype.toString.call(undefined)); // '[object Undefined]'
console.log(Object.prototype.toString.call(Symbol("a"))); // '[object Symbol]'

有了上面的基础,我们就可以统一调用 Object.prototype.toString 方法来获取数据具体的类型,然后把多余的字符去掉即可,只取 [object Xxx] 里的 Xxx

不过使用 Object.prototype.toString 判断原始类型时,会进行装箱操作,产生额外的临时对象,为了避免这一情况的发生,我们也可以结合 typeof 来判断除了 null 之外的原始类型,于是最后的代码实现如下:

function getType(target) {
  // 先进行 typeof 判断,如果是基础数据类型,直接返回。
  const type = typeof target;
  if (type !== "object") {
    return type;
  }

  // 如果是引用类型或者 null,再进行如下的判断,正则返回结果,注意最后返回的类型字符串要全小写。
  return Object.prototype.toString
    .call(target)
    .replace(/^\[object (\S+)\]$/, "$1")
    .toLocaleLowerCase();
}