直接在项目中使用——2023年JS如何实现类型判断

117 阅读8分钟

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

  • 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'

你会发现 null[] 也被判定为 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 


**正则表达式(Regular Expression,在代码中常简写为regex、regexp或RE)**
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 可以判断引用类型,把它俩结合起来,就可以实现精准判断类型的 getType 函数了。

别忘了,还有个 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(); 
}

上面代码中从 [object Xxx] 里取出 Xxx 用到了 replace 方法和正则,关于正则这种“八股文”,想要进阶,躲是躲不掉的,但是不建议同学们去硬啃,太枯燥,可以了解一些用得较多的方法和写法,知道大概的写法后,再结合类似这个网站这样的正则可视化去分析该怎么写。关于正则,具体的内容会放到课程后面讲字符串、正则相关面试题时介绍。

其实,上面的函数还可以换一种写法,代码如下

// ...
return Object.prototype.toString 
.call(target) 
.match(/\s([a-zA-Z]+)\]$/)[1] // 这种写法也可以。
.toLocaleLowerCase();

为什么要使用 call

解答一下上文留下的疑问,为什么要写成 Object.prototype.toString.call(xxx) 的形式来判断 xxx 的类型?

call 是函数的方法,是用来改变 this 指向的,用 apply 方法也可以。

如果不改变 this 指向为我们的目标变量 xxxthis 将永远指向调用的 Object.prototype,也就是原型对象,对原型对象调用 toString 方法,结果永远都是 [object Object],如下代码所示:

Object.prototype.toString([]); // 输出 '[object Object]' 
不调用 call,this 指向 Object.prototype,判断类型为 ObjectObject.prototype.toString.call([]); // 输出 '[object Array]' 
调用 call,this 指向 [],判断类型为 Array 
Object.prototype.toString(1); // 输出 '[object Object]' 
不调用 call,this 指向 Object.prototype,判断类型为 ObjectObject.prototype.toString.call(1); // 输出 '[object Number]'
调用 call,this 指向包装对象 Number {1},判断类型为 Number

可以重写 Object.prototype.toString 方法,把 this 打印出来验证一下,代码如下所示

// 重写 Object.prototype.toString 方法,只打印 
this Object.prototype.toString = function () { 
console.log(this); 
}; 
// 引用类型
Object.prototype.toString([]); // 输出 Object.prototype 
Object.prototype.toString.call([]); // 输出 [] 
// 原始类型 
Object.prototype.toString(1); // 输出 Object.prototype
Object.prototype.toString.call(1); // 输出 Number {1},
这里的 Number {1},是一个包装类,把基本类型用它们相应的引用类型包装起来,使其具有对象的性质

拓展

常见面试题 1:JavaScript 有哪些数据类型

答: JavaScript 的数据类型分为原始类型和对象类型。

原始类型有 7 种,分别是:

对象类型(也称引用类型)是一个泛称,包括数组、对象、函数等一切对象。

常见面试题 2:typeof null 的结果是什么

答:

typeof null; // 'object'

null 作为一个基本数据类型为什么会被 typeof 运算符识别为对象类型呢?

事实上,这是第一版 JavaScript 留下来的一个 bug。

JavaScript 中不同对象在底层都表示为二进制,而 JavaScript 中会把二进制前三位都为 0 的判断为 object 类型,而 null 的二进制表示全都是 0,自然前三位也是 0,所以执行 typeof 时会返回 'object'

那为啥那一堆设计语言的大佬们会放任这个 bug 存在这么多年呢?

因为这个 bug 牵扯了太多的 Web 系统,一旦改了,会产生更多的 bug,令很多系统无法工作,也许这个 bug 永远都不会修复了。

判断一个类型为 null 可以这么写,直接判断变量全等于 null

let a = null;
if (a === null) {
  // do something
}

常见面试题 3:原始类型和引用类型的区别是什么

答:

类型原始类型对象类型
不可改变可以改变
属性和方法不能添加能添加
存储值地址(指针)
比较值的比较地址的比较

常见面试题 4:typeof 和 instanceof 的区别是什么

答:

  • typeof 运算符用来判断数据的类型。

  • instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上,也可以用来判断数据的类型。

    • typeof 返回一个变量的类型字符串,instanceof 返回的是一个布尔值。
    • typeof 可以判断除了 null 以外的基础数据类型,但是判断引用类型时,除了 function 类型,其他的无法准确判断。
    • instanceof 可以准确地判断各种引用类型,但是不能正确判断原始数据类型。

常见面试题 5:Symbol 解决了什么问题

答:Symbol 是 ES6 时新增的特性,Symbol 是一个基本的数据类型,表示独一无二的值,主要用来防止对象属性名冲突问题。

ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的属性,新属性的名字就有可能与现有属性的名字产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入 Symbol 的原因之一。

Symbol 值通过 Symbol 函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。

代码演示如下:

const obj = {
  name: "lin",
  age: 18,
};

obj.name = "xxx"; // 给 obj.name 赋值,把以前的 name 覆盖了
console.log(obj); // { name: 'xxx', age: 18 }
const obj = {
  name: "lin",
  age: 18,
};
const name = Symbol("name");

obj[name] = "xxx"; // 使用 Symbol,不会覆盖

console.log(obj); // { name: 'lin', age: 18, Symbol(name): 'xxx' }
console.log(obj.name); // 'lin'
console.log(obj[name]); // 'xxx'