判断一个数据的类型,比较常用的有下面几种方式:
typeofinstanceofObject.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 指向为我们的目标变量 xxx,this 将永远指向调用的 Object.prototype,也就是原型对象,对原型对象调用 toString 方法,结果永远都是 [object Object],如下代码所示:
Object.prototype.toString([]); // 输出 '[object Object]'
不调用 call,this 指向 Object.prototype,判断类型为 Object。
Object.prototype.toString.call([]); // 输出 '[object Array]'
调用 call,this 指向 [],判断类型为 Array
Object.prototype.toString(1); // 输出 '[object Object]'
不调用 call,this 指向 Object.prototype,判断类型为 Object。
Object.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'