面试官让我手写一个类型判断,我心中狂喜

4,002 阅读6分钟

JS中的类型判断也是咱的老朋友了,如果要你说两句,那便是啥玩意typeofinstanceof的,张口就来,但你真的掌握了每一种类型判断,并熟知其原理吗?就比如说Object.prototype.toString.call(),当面试官问其中的 call有什么作用时,咱们又该如何游说呢。

typeof原理

所有的类型判断里当属typeof最为经典,其最为出名之处就是这玩意不顶用,typeof居然会将null判定为object,但这到底是什么原因?实际上,这就是JS这门编程语言的官方团队造出来的一个bug。

let s = '123'    
let n = 123     
let f = true      
let u = undefined  
let nu = null      
let sy = Symbol(123) 
let big = 1234n  
let obj = {}
let arr = []
let fn = function(){}
let date = new Date();

console.log(typeof(s));  // string
console.log(typeof(n));  // number
console.log(typeof(f))   // boolean
console.log(typeof(u))   // undifined
console.log(typeof(sy))  // symbol
console.log(typeof(big)) // bigint

console.log(typeof(nu));  //将null判断为object

//以下为引用类型,除了function,一律被判断为object
console.log(typeof(obj))  //object
console.log(typeof(arr))  //object
console.log(typeof(data)) //object

// 所有引用类型只能判断function
console.log(typeof(fn))   //function

//

原因就是:typeof会把所有传进去的值都转成二进制,而当年JS制定的规则便是:原始类型的被转为二进制的前面三个值绝对不为零,而typeof会把前三位为零的类型全部认定为对象,null这个类型又是JS语言后来引入的。遵循其他语言的原则,JS语言将null的二进制值定为一长串的0,因此typeof在判断时会将其认定为object

因此,面试官一般不会询问判断方法 typeof 的原理。

手写一个instanceof

相较于typeofinstanceof有其优越之处,也有其不足之处,比如,instanceof只能判断引用类型,并且是通过原型链查找来判断类型。

let s = '123'    
let n = 123     
let f = true      
let u = undefined  
let nu = null      
let sy = Symbol(123) 
let big = 1234n  
let obj = {}
let arr = []
let fn = function(){}
let date = new Date();

console.log(s instanceof String);     // false instanceof 不能判断原始类型

// 以下为引用类型

console.log(obj instanceof Object) ;  // true
console.log(arr instanceof Object);   // true
console.log(fn instanceof Function);  // true
console.log(date instanceof Date);    // true

首先要知道每个类型的原型都不相同。

instanceof的原理具体来说就是instanceof会顺着原型链查找出其继承的原型,看这个原型到底是属于StringNumberBoolean等具体类型的哪一种,如果被判断的类型与我们期望的类型的原型相同,输出true,否则输出false。

关于原型,原型链的定义可以参考这篇文章:juejin.cn/post/737503…

以下则为手写myinstanceof代码:

function myinstanceof(L, R){
    while(L !== null){
        if(L.__proto__ === R.prototype){
            return true;
        }
        L = L.__proto__
    }
    return false
} 
console.log(myinstanceof([], Array))  // true

令人奇怪的是,为什么原型的判断中牵扯到while?熟知原型链的友友应该不难理解,正如之前所说,instanceof 是通过原型链查找来判断类型,那岂不知显示原型生隐式原型,如此往复,子子孙孙无穷尽也,正如下代码,如果用舍弃 while 转而用if进行进行判断,仅仅是三代原型的传递,就要用到三次if判断,可想而知原型链一长,代码都打不过来的光景。

以下为不正确的myinstanceof手写代码:

function B(){}
let b = new B();

A.prototype = b;
function A(){}
let a = new A();

function myinstanceof(L,R){
    if(L.__proto__ === R.prototype){
        return true;
    } else {
        if(L.__proto__.__proto__ === R.prototype) {
            return true
        } else {
            if(L.__proto__.__proto__.__proto__ === R.prototype)
            return true
        }
    
    }
}

console.log(myinstanceof(a, Object))    // true

最完美的判断方式:Object.prototype.toString.call()

类型判断中最为完美的毋庸置疑,当属 Object.prototype.toString.call(),这是一种属于object原型上的方法。它能做到正确的判断出每一种类型。

let s = '123'    
let n = 123     
let f = true      
let u = undefined  
let nu = null      
let sy = Symbol(123) 
let big = 1234n  
let obj = {}
let arr = []
let fn = function(){}
let date = new Date();

console.log(Object.prototype.toString.call(s));   //  [object String]
console.log(Object.prototype.toString.call(n));   //  [object Number]
console.log(Object.prototype.toString.call(f));   //  [object Boolean]
console.log(Object.prototype.toString.call(u));   //  [object Undefined]
console.log(Object.prototype.toString.call(nu));  //  [object Null]

console.log(Object.prototype.toString.call(obj));  // [object Object]
console.log(Object.prototype.toString.call(arr));  // [object Array]
console.log(Object.prototype.toString.call(fn));   // [object Function]
console.log(Object.prototype.toString.call(date)); // [object Date]

这种判断方法的完整代码为 Object.prototype.toString.call(),关于它的原理,我们来看看官网是怎么对Object.prototype.toString() 进行解释的:

8948b1aab104d8597938de163aad085.png

翻译成人话就是:

  1. 如果你传进来的值为undefined的话,直接返回一个[object Undefined]。

  2. 如果你传进来的值为null的话,直接返回一个[object Null]。

  3. 如果你既不是undefined又不是null的话,JS将调用ToObject方法,将O作为 ToObject(this)的执行结果。

ToObject的执行机理简单来说就是,传进来一个boolean类型会创建一个boolean包装类对象,传进来Number会创建一个Number字面量,传进来一个String会创建一个字符串字面量,传进来一个对象就会创建这个对象。总而言之,任何传进去的任何值都会转换为对象。

  1. 定义一个class作为内部属性[[class]]的值,用于承接传进来的值。

  2. 返回由 "[" objectclass"]" 组成的字符串。

现在我们知道 Object.prototype.toString()的原理了,但是 Object.prototype.toString.call()后面加的这个call又有什么作用呢?让我们先试着输出一下:

1720141205510.png

可以看到令人啼笑皆非的一幕发生了,Object.prototype.toString()将 123 判断为object,而Object.prototype.toString.call()则输出了正确的判断,这又是什么原理?

这正是Object.prototype.toString()执行过程中调用ToObject()的结果,上文提到,ToObject()会把你传进来的值都创建为相应的类型,123 传进来被创建为String,而字符串类型在V8眼里可不就是对象嘛。

需要注意的是所有原始类型都会被V8以对象形式创建,关于这点可以参考这篇文章juejin.cn/post/737397…

现在我们来说明 call 在此的作用,我们都知道.call(obj)的作用是将 .call 之前的函数中的 this 指向 obj,但也可以说是把 .call 之前的函数方法借给 obj 去使用,因此在Object.prototype.toString.call()中,toString()是这样被调用的:

Object.prototype.toString.call(obj)

obj.toString()

就像上文对123的判断,当没有calltoString是在被123实例对象调用,而当添加上call 后,toString就变成是被123的原型所调用。因此,call确保了类型判断是被原型在调用,从而能输出正确的值。

Array.isArray()

最后一个要讲的就是Array.isArray(),这是一个隶属于 Array 构造函数的静态方法,遗憾的是,它只能判断数组。

let arr = []
let s = '123' 


console.log(Array.isArray(arr));  // true
console.log(Array.isArray(s));    // false

总结:

typeof

  • 可以判断除 null 之外的所有原始类型。
  • 除了function其他所有的引用类型都会被判断成object。
  • typeof是通过将值转换为二进制后判断其二进制前三位是否为0,是则为object

instanceof

  • 只能判断引用类型。
  • 通过原型链查找来判断类型。

Object.prototype.toString()

  • 一种Object原型上的方法。

Array.isArray()

  • 一个隶属于 Array 构造函数的静态方法。

以上便是 typeofinstanceofObject.prototype.toString.call()Array.isArray() 这四种类型判断。虽然我说其中Object.prototype.toString()的判断最为完美全面,但在实际应用之中它们各有优劣之处,应用场景也各有不同。