以下内容并非原创,只是为了整理记录复习。
这道题主要考察 typeof,instanceof 和 Object.prototype.toString。先看答案:
function classof(o) {
if (o === null) return "null";
if (typeof o !== "object") return typeof o;
else
return Object.prototype.toString
.call(o)
.slice(8, -1)
.toLocaleLowerCase();
}
测试类型校验结果:
classof(2020); // number
classof("石头加油"); // string
classof(true); // boolean
classof(undefined); // undefined
classof(null); // null
classof(Symbol("没毛病!")); // symbol
classof(1n); // bigint
classof({}); // object
classof(classof); // function
classof([]); // array
classof(new Date()); // date
// 还是没法细分自定义类,如果需要的话可以再结合constructor.name继续封装
classof(new classof()); // object
看完答案会有几个疑问:
- 为什么先判断 null?
- 为什么 typeof 返回值不是 object 就直接返回 typeof 的值?
- 为什么最后用 Object.prototype.toString,为什么要 call?
- 说好的 instanceof 呢?
往下看,我们一一解答。
一、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'
从上面例子,前6个都是基础数据类型。虽然typeof null
为object
,但这只是JavaScript
存在的一个悠久 Bug
,不代表 null
就是引用数据类型,并且null
本身也不是对象。
所以,null
在 typeof
之后返回的是有问题的结果,不能作为判断null
的方法。如果你需要在 if
语句中判断是否为 null
,直接通过===null
来判断就好。
这就是为什么先要判断是不是null
!
同时,可以发现引用类型数据,用typeof
来判断的话,除了function
会被识别出来之外,其余的都输出object
。
如果我们想要判断一个变量是否存在,可以使用typeof
:(不能使用if(a)
, 若a
未声明,则报错)。
if(typeof a != 'undefined'){
//变量存在
}
二、instanceof
instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。
使用如下:
object instanceof constructor
object
为实例对象,constructor
为构造函数。
构造函数通过new
可以实例对象,instanceof
能判断这个对象是否是之前那个构造函数生成的对象。
// 定义构建函数
let Car = function() {}
let benz = new Car()
benz instanceof Car // true
let car = new String('xxx')
car instanceof String // true
let str = 'xxx'
str instanceof String // false
instanceof
的实现原理,就是顺着原型链去找,直到找到相同的原型对象,返回true
,否则为false
。
可以参考下面:
function myInstanceof(left, right) {
// 这里先用typeof来判断基础数据类型,如果是,直接返回false
if(typeof left !== 'object' || left === null) return false;
// getProtypeOf是Object对象自带的API,能够拿到参数的原型对象
let proto = Object.getPrototypeOf(left);
while(true) {
if(proto === null) return false;
if(proto === right.prototype) return true;//找到相同原型对象,返回true
proto = Object.getPrototypeof(proto);
}
}
三、Object.prototype.toString
typeof
与instanceof
都是判断数据类型的方法,区别如下:
typeof
会返回一个变量的基本类型,instanceof
返回的是一个布尔值instanceof
可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型- 而
typeof
也存在弊端,它虽然可以判断基础数据类型(null
除外),但是引用数据类型中,除了function
类型以外,其他的也无法判断
可以看到,上述两种方法都有弊端,并不能满足所有场景的需求。
如果需要通用检测数据类型,可以采用Object.prototype.toString
,调用该方法,统一返回格式“[object Xxx]”
的字符串。
例子:
Object.prototype.toString.call({}) // "[object Object]"
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call('1') // "[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]"
四、Object.prototype.toString 原理解析
在toString方法被调用时,会执行以下几个操作步骤~
-
获取
this指向
的那个对象的[[Class]]
属性的值。 (这也是我们为什么要用call改变this指向的原因) -
计算出三个字符串"[object "、 第一步的操作结果Result(1)、 以及 "]" 连接后的新字符串。
-
返回第二步的操作结果Result(2),也就是类似
[object className]
这种格式字符串。
[[Class]]
类属性
对象的类属性(class attribute)是一个字符串,用以表示对象的类型信息。ES3和ES5都没有提供设置这个属性的方法,并只有一种间接的方法可以查询到它。默认的toString方法(继承自Object.prototype)返回了如下格式的字符串:[object class] 因此,想要获得对象的类,可以调用对象的toString方法,然后提取已返回字符串的第8个到倒数第2个位置之间的字符。
Object.prototype.toString.call(target).slice(8, -1);
综上,[[Class]]
是一个字符串值,表明了该对象的类型。他是一个内部属性,所有的对象(原生对象和宿主对象)都拥有该属性,且不能被任何人修改。在规范中,[[Class]]
是这么定义的:内部属性 描述。
宿主对象也包含有意义的“类属性”,但这和具体的JavaScript实现有关。
因为js中每个类型都有自己私有的[[Class]]
属性,而且这个class是不能被任何人修改的,所以tostring
方法是检测属性类型最准确的方法,比instanceof
还准确。
他也可以细分内置构造函数创建的类对象:
通过内置构造函数(Array、Date等)创建的对象包含“类属性”(class attribute),他与构造函数的名称相匹配(这里也是我从contructor.name来区分数据类型的启发点)。
但是,他无法区分自定义对象类型。
通过对象直接量和Object.create创建的对象的类属性是“object”,那些自定义构造函数创建的对象也是一样。类属性都是“Object”,因此对于自定义类来说,没办法通过类属性来区分对象的类。这个时候还是得用instanceof
。
为什么用call
这里用call
是为了改变toString
函数内部的this
指向,其实也可以用apply
。
之所以必须改变this指向是因为,toString
内部是获取this
指向那个对象的[[Class]]
属性值的,如果不改变this
指向为我们的目标变量,this
将永远指向调用toString
的prototype
。
另外也是因为,很多对象继承的toString
方法重写了,为了能调用正确的toString
,才间接的使用call/apply
方法。
代码演示:
Object.prototype.toString = function () {
console.log(this);
};
const arr1 = [];
Object.prototype.toString(arr1); // 打印(即this指向) Object.prototype
Object.prototype.toString.call(arr1); // 打印(即this指向) arr1
为什么null也能判断?undefined和null这两个原始值不是没有属性值吗?
因为每一个类型都有自己唯一的特定 类属性(class attribute
) 标识,null也有、undefined也有。
该方法判断类型的缺陷
他虽然判断类型完善,但也不是没有缺点,主要有两点:
-
tostring会进行
装箱操作
,产生很多临时对象(所以真正进行类型转换时建议配合typeof
来区分是对象类型还是基本类型,见最后代码) -
无法区分自定义对象类型,用来判断这类对象时,返回的都是
Object
(针对“自定义类型”可以采用instanceof
区分)
扩展:什么是装箱操作?
-
“装箱”
就是把基本类型用它们相应的引用类型包装起来,使其具有对象的性质。可以简单理解为 “包装类” 。 (详细信息,可参考阅读《JavaScript权威指南-3.6 包装对象》相关解析,这里不再延伸)。 -
而对应的“拆箱”,就是将引用类型的对象简化成值类型的数据。