JS 类型判断大乱斗:typeof、instanceof、toString 到底该用谁?
写 JS 的人应该都被 typeof null === 'object' 坑过吧。
我第一次发现的时候整个人都不好了。null 明明是空值,你告诉我它是 object?后来才知道这玩意儿是个历史遗留 bug,但就因为这个坑,让我开始认真研究 JS 的类型判断到底是怎么回事。
结果发现好家伙,typeof、instanceof、Object.prototype.toString.call(),三个方法各有各的脾气。今天借着我的学习笔记,把这三种方法彻底捋一遍。
JS 的类型分两种
这个得先搞清楚。JS 的值分成两大类:
原始类型:string、number、boolean、null、undefined、BigInt、Symbol
引用类型:Array、function、object、Date 等等
原始类型存的是具体的值,引用类型存的是内存地址。这个区别直接导致了后面判断方式的差异。
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 说:你是对象。行吧。🤡
所以在实际开发中,如果需要判断一个值是不是真的对象(排除 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。
但是 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的实现必须会
说到底,每种方法都有自己的定位。typeof 的开发体验其实挺好的,就是 null 这个坑让人无语。要是 JS 刚设计的时候能把这个 bug 修了,估计能少很多面试题(笑)。
你有没有被 typeof null 坑过的经历?欢迎在评论区分享你的翻车故事。