# JS 类型判断大乱斗:typeof、instanceof、toString 到底该用谁?

1 阅读5分钟

JS 类型判断大乱斗:typeof、instanceof、toString 到底该用谁?

写 JS 的人应该都被 typeof null === 'object' 坑过吧。

我第一次发现的时候整个人都不好了。null 明明是空值,你告诉我它是 object?后来才知道这玩意儿是个历史遗留 bug,但就因为这个坑,让我开始认真研究 JS 的类型判断到底是怎么回事。

结果发现好家伙,typeofinstanceofObject.prototype.toString.call(),三个方法各有各的脾气。今天借着我的学习笔记,把这三种方法彻底捋一遍。

JS 的类型分两种

这个得先搞清楚。JS 的值分成两大类:

原始类型stringnumberbooleannullundefinedBigIntSymbol

引用类型ArrayfunctionobjectDate 等等

原始类型存的是具体的值,引用类型存的是内存地址。这个区别直接导致了后面判断方式的差异。

image.png

typeof——原始类型的侦察兵

typeof 处理原始类型的时候,基本靠谱:

let s = 'hello'       // typeof → 'string'
let num = 123         // typeof → 'number'
let f = true          // typeof → 'boolean'
let u = undefined     // typeof → 'undefined'
let sy = Symbol(1)    // typeof → 'symbol'
let big = 12343242n   // typeof → 'bigint'

除了 null,所有原始类型它都能准确判断。

函数它也能认出来——typeof function(){} 返回 'function'

但到了引用类型这边,它就废了:

let arr = []
let obj = {}
let n = null

typeof arr // → 'object'
typeof obj // → 'object\'
typeof n   // → 'object'  ← 就是这个坑

所有的引用类型,在 typeof 眼里都是 'object',函数除外。

那为啥 typeof null 也返回 'object'?这个得说到 typeof 的实现原理。

typeof 是通过把值转成二进制来判断的。二进制前三位是 0 的统一被认为是对象类型。所有的引用类型,二进制前三位确实都是 0。但 null 呢?它比较特殊——二进制是一整串 0,所以前三位也是 0……

于是 typeof 说:你是对象。行吧。🤡

image.png 所以在实际开发中,如果需要判断一个值是不是真的对象(排除 null),得这么写:

if (typeof o === 'object' && o !== null) {
    console.log('这是一个真的对象')
}

typeof 适合快速判断原始类型,遇到引用类型和 null 就抓瞎了。那引用类型怎么判断?看下面。

instanceof——引用类型的猎犬

typeof 搞不定引用类型,instanceof 来补位。

let arr = []
let obj = {}
let fn = function() {}

arr instanceof Array    // → true
obj instanceof Object   // → true
fn instanceof Function  // → true

但注意,arr instanceof Object 也是 true。因为数组本身就是对象的一种,原型链上能找到 Object。

instanceof 的原理是通过原型链来查找的。

arr instanceof Array 做的是:

  • 沿着 arr.__proto__ 一路往上找
  • 看能不能找到 Array.prototype
  • 找到了就是 true,找不到就是 false

所以 arr instanceof Object 也是 true,因为 Array.prototype.__proto__ === Object.prototype

image.png

但是 instanceof 有个局限——它判断不了原始类型:

'hello' instanceof String  // → false
123 instanceof Number      // → false

这个好理解,原始类型没有 __proto__ 嘛。

既然理解了原理,就可以自己手写一个:

function myInstanceof(l, r) {
    if (typeof l !== 'object' && typeof l !== 'function' || l == null) {
        return false
    }

    while (l !== null) {
        if (l.__proto__ === r.prototype) {
            return true
        }
        l = l.__proto__
    }
    return false
}

myInstanceof([], Array)   // → true
myInstanceof([], Object)  // → true
myInstanceof({}, Array)   // → false

这个基本上是面试手写题的常客。

Object.prototype.toString.call()——一招通杀

那有没有一种方法,既能判断原始类型,又能判断引用类型?

有。Object.prototype.toString.call() 全都能搞定。

let s = 'hello'
let num = 123
let n = null
let u = undefined
let arr = []
let obj = {}
let fn = function() {}

Object.prototype.toString.call(s)   // → '[object String]'
Object.prototype.toString.call(num) // → '[object Number]'
Object.prototype.toString.call(n)   // → '[object Null]'
Object.prototype.toString.call(u)   // → '[object Undefined]'
Object.prototype.toString.call(arr) // → '[object Array]'
Object.prototype.toString.call(obj) // → '[object Object]'
Object.prototype.toString.call(fn)  // → '[object Function]'

全部准确识别,一个不漏,包括 null 和 undefined。

它是怎么做到的?我自己的理解是这样:

Object.prototype.toString 内部大概做了这么几件事:

// 思路大概是这样,不是源码哈
Object.prototype.toString = function() {
    // 如果 this 是 null,直接返回 "[object Null]"
    // 如果 this 是 undefined,返回 "[object Undefined]"
    
    const O = ToObject(this)   // 把 this 转成对象
    const class = O.[[Class]]  // 获取内部的 [[Class]] 属性
    return "[object " + class + "]"
}

当传 123 进去时,call(123) 把 this 指向 123,内部先转成 new Number(123),然后取它的内部属性 [[Class]],拿到 Number,返回 [object Number]

这种写法有点长,我一般会封装一下:

function getType(x) {
    return Object.prototype.toString.call(x).slice(8, -1)
}

getType([])         // → 'Array'
getType(null)       // → 'Null'
getType(123)        // → 'Number'

数组的判断有个专用方法

因为判断数组太常见了,ES5 直接给 Array 加了个静态方法 Array.isArray()

Array.isArray([])  // → true
Array.isArray({})  // → false

它是 Array 上的静态方法,不是实例方法。所以是 Array.isArray(x),不能写成 x.isArray()

面试常考

怎么判断一个变量是数组?

三种方式:

// 最推荐
Array.isArray(arr)

// 通用方案
Object.prototype.toString.call(arr) === '[object Array]'

// 有坑的方案
arr instanceof Array  // 如果跨 iframe,这个可能失效

说到这个 instanceof 的坑,如果你在一个 iframe 里创建的数组,用主页面的 instanceof 去判断,可能会返回 false。因为两个页面的 Array 构造函数不一样。这也是为什么更推荐 Array.isArray()

typeof null 为什么返回 "object"?

前面说了——null 的二进制全零,前三位是 0,被 typeof 归为 object 了。

总结一下

在项目里我一般这么用:

  • 判断原始类型 → typeof,但得注意 null 这个例外
  • 判断引用类型 → Object.prototype.toString.call() 最稳
  • 单纯判断数组 → Array.isArray()
  • 面试手写 → instanceof 的实现必须会

image.png

说到底,每种方法都有自己的定位。typeof 的开发体验其实挺好的,就是 null 这个坑让人无语。要是 JS 刚设计的时候能把这个 bug 修了,估计能少很多面试题(笑)。

你有没有被 typeof null 坑过的经历?欢迎在评论区分享你的翻车故事。