从原理、优缺点、用途三方面解读类型检测,妈妈再也不用担心面试官问我类型检测了

92 阅读6分钟

前言

我们平时在写代码时要注意变量的类型,如一个函数需要传入两个参数name和age然后将其打印出来,这时name应该为String类型而age应该为Number类型,那么我们该如何判断我们传入的变量的类型呢?

function introduce(name, age) {
  console.log(`my name is ${name}. I'm ${age} years old`)
}

typeof

首先最简单的判断数据类型的方式是typeof操作符

let number = 1234
let string = 'LDec.27'
let boolean = true
let Null = null
let Undefined = undefined
let symbol = Symbol(111)
let bigint = BigInt(111)
let obj = {}
let func = function () {console.log("hello")}
let arr = []
let date = new Date()
let reg = new RegExp()

console.log(`number的类型为 ${typeof number}`) // 'number'
console.log(`string的类型为 ${typeof string}`) // 'string'
console.log(`boolean的类型为 ${typeof boolean}`) // 'boolean'
console.log(`Undefined的类型为 ${typeof Undefined}`) // 'undefined'
console.log(`symbol的类型为 ${typeof symbol}`) // 'symbol'
console.log(`bigint的类型为 ${typeof bigint}`) // 'bigint'
console.log(`null的类型为 ${typeof null}`) // 'object'
console.log(`obj的类型为 ${typeof obj}`) // 'object'
console.log(`func的类型为 ${typeof func}`) // 'function'
console.log(`arr的类型为 ${typeof arr}`) // 'object'
console.log(`date的类型为 ${typeof date}`) // 'object'
console.log(`reg的类型为 ${typeof reg}`) // 'object'

说原理

原理: 不同的数据在底层都表示为二进制,在Javascript中二进制前(低)三位存储其类型信息。

  • 000:对象

  • 010:浮点数

  • 100:字符串

  • 110:布尔

  • 1:整数

    而null和undefined类型会有所不同

  • null: 其用二进制表示全部为0

  • undefined: 用 −2^30 整数来表示

优缺点

通过上面对typeof原理的讲解和代码演示我们可以发现typeof的优点和缺点很明显

优点

typeof的优点就是它可以很方便的帮我们判断某一个原始类型值的类型(除null外)

缺点

  1. 历史悠久的关于判断null类型的bug
  2. 我们发现typeof在判断引用类型的值时很乏力,除了判断函数外永远都是返回object,我们更想知道的是其具体类型。如:[1,2]是不是Array。所以我们判断引用类型数据的类型时更多会用instanceof操作符来判断(下文会讲到)

讲用途

通过上面对优缺点的分析,用途不就来了吗?扬长避短我们尽量用typeof来判断除null以外的其他基本数据类型

instanceof

第二种判断数据类型的方式是instanceof操作符

说原理

原理用一句话来概括就是:构造函数的显式原型等于其实例对象的隐式原型,沿着原型链向上查找。

优缺点

讲优缺点之前先上一段代码

console.log(123 instanceof Number) // false
console.log("123" instanceof String) // false
console.log(new Number(123) instanceof Number) // true
console.log(new Number(123) instanceof Object) // true
console.log(true instanceof Boolean) // false
console.log([] instanceof Array); // true
console.log([] instanceof Object); // true
console.log({} instanceof Object) // true
console.log(null instanceof Null) // TypeError: Right-hand side of 'instanceof' is not an object

我们发现instanceof在判断引用类型的数据时好像确实可以达到我们的需求,但是他在判断基本数据类型方面好像不太给力,明明就是Number类型的值123为什么判断会是false呢?

我们回到其原理上来看:构造函数的显式原型等于其实例对象的隐式原型,沿着原型链向上查找

(%J0IVC[GXD$][]`DJ9L)BI.png

我们在浏览器上直接输入一些值并打印,眼尖的小伙伴可能发现了原始类型的值打印出来比引用类型的值少了个"▶"这意味着我们直接将一个变量赋值为原始类型是找不到它的隐式原型的所以才会返回false

所以我们在使用instanceof操作符判断原始类型时要使用new操作符通过构造函数创建变量

再有一点就是,我们发现好像所有类型的值判断xxx instanceof Object最后都会返回true,这是因为原型链的顶端就是Object,而instanceof的原理是沿着原型链向上查找,所以xxx instanceof Object最后都会返回true

最后我们发现instanceof操作符也是无法判断null类型的,会报错。

优点

优点就是我们可以使用instanceof操作符可以更加具体的判断是哪种object类型

缺点

  1. 其实instanceof设计的初衷本就不是用来判断数据类型的,其 设计初衷是为了判断某个构造函数是否在某一实例对象的原型链上(重点)(对原型链不熟悉的可以看我的上一篇文章)
  2. 如果想要判断原始类型的值需要使用构造函数来创建实例对象否则只会返回false,并且由于新增了Symbol和BigInt类型对其使用new操作符会报错,所以还是不建议使用instanceof来判断原始类型的值(typeof不香吗?)
  3. xxx instanceof Object永远都会返回true,这给我们判断带来了不确定型,如果我们盲目判断其为Object类型的话会返回true但是可能会发生误判
  4. instanceof依旧不能判断null类型

讲用途

从上述表达来看,instanceof似乎比typeof还要不适合用来判断数据类型,但是我们依旧可以用它来判断引用类型的值的具体类型

手写instanceof

// 手写instanceof
function myInstanceof(left, right) {
  // left为我们想判断的值,right为我们推断的类型
  // 拿到left的隐式原型
  pro = left.__proto__
  // 判断若为原始类型直接返回false
  if(left === null || (typeof left !== "object" && typeof left !== "function")) return false
  while(pro !== null) { // 只要pro不为null就一直沿着原型链往上查找
    // 如果隐式原型等于显式原型则返回true
    if(pro == right.prototype) return true
    // 不等于则沿着原型链继续往上查找
    pro = pro.__proto__
  }
  return false
}

console.log(myInstanceof(123, Number)) // false
console.log(myInstanceof("123", String)) // false
console.log(myInstanceof(true, Boolean)) // false
console.log(myInstanceof([], Array)); // true
console.log(myInstanceof({}, Object)) // true
console.log(myInstanceof(function foo() {console.log("hello")}, Function)) // true

上述两种判断类型的方法各有利弊,难道就没有一种两全其美的方法既可以检测原始类型又可以检测引用类型吗?

toString

先看一段代码

console.log({}.toString()) // '[object Object]'
console.log([1,2].toString()) // '1,2'
console.log(function foo() {return 1}.toString()) // 'function foo() {return 1}'
console.log(new Date().toString()) // 'Thu May 05 2022 15:39:58 GMT+0800 (中国标准时间)'

说原理

Object构造函数中有toString方法,其会返回 [object Type],但是其他的引用类型中的toString方法会被重写,所以我们可以使用call方法改变this指向,来达到我们的目的

console.log(Object.prototype.toString.call(123)) // '[object Number]'
console.log(Object.prototype.toString.call("123")) // '[object String]'
console.log(Object.prototype.toString.call(null)) // '[object Null]'
console.log(Object.prototype.toString.call({})) // '[object Object]'
console.log(Object.prototype.toString.call([])) // '[object Array]'
console.log(Object.prototype.toString.call(function foo(){return 1})) // '[object Function]'
console.log(Object.prototype.toString.call(new Date())) // '[object Date]'

优缺点

这个办法不但可以判断引用类型的值的类型,还可以判断原始类型值的类型,甚至连typeof和instanceof无法判断的null类型也可以判断出来

简单封装一下

为了能直接得到数据类型而不是[object xxxxx]的格式我们可以将toString函数简单封装一下

function getType(value) {
  let str = Object.prototype.toString.call(value)
  // 我们要的结果从字符串第8位开始并且要舍去最后一个字符']',所以是(8,-1)
  let type = str.slice(8, -1) 
  return type
}

console.log(getType(123)); // Number
console.log(getType("123")); // String
console.log(getType(true)); // Boolean
console.log(getType(Symbol("name"))); // Symbol
console.log(getType([])); // Array
console.log(getType({})); // Object
console.log(getType(null)); // Null

最终我们找到了一种两全其美的办法完美的实现了对数据类型的检测

参考文章

# 【JS 进阶】你真的掌握变量和类型了吗

# 浅谈 instanceof 和 typeof 的实现原理