Skip to content

25. 深入类型判断

0. 前言

提到类型判断,回忆下 JS 中的 8 种数据类型:

  • 7 种基本数据类型:
    • string
    • number
    • boolean
    • null
    • undefined
    • symbol
    • bigint
  • 1 种对象类型:
    • object:plain object、array、function、arraylike、Map、Set...

众所周知, JS 中是没有静态类型的,但有时候有需要对变量的类型进行判断,于是 JS 内置了 typeofinstanceof 等方法来对类型进行判断,但坑不少,对于新手很容易跳进去。

例如,下面这几个结果可能会让你感到诡异:

js
typeof NaN       // number
typeof [1, 2, 3] // object
typeof null      // object

Function instanceof Object // true
Object instanceof Function // true
1
2
3
4
5
6

别急,且听我一一道来。

1. typeof

语法:typeof(x)typeof x

typeof 只能判断 5 种基本数据类型(string、boolean、bigint、symbol、undefined)和 函数类型(不过 function 不独立为一类类型,而是属于 object 类型),不能判断另外 3 类基本类型(null、NaN、number)。除此之外的 其他对象都返回 object 类型

  1. typeof 可以判断的 5 类基本类型:
js
const isString = x => typeof x === 'string'
const isBoolean = x => typeof x === 'boolean'
const isUndfined = x => typeof x === 'undefined'
const isSymbol = x => typeof x === 'symbol'
const isBigInt = x => typeof x === 'bigint'
1
2
3
4
5
  1. 不能判断其他 3 类基本类型

(1)不能判断 number 和 NaN 的原因:

js
typeof NaN // number
1

解释

4.4.27 NaN: Number value that is an IEEE 754-2019 “Not-a-Number” value. ——ECMA TC39 NaN ECMA 数字类型中根据 IEE754 的标准规定,NaN 是一个 number value。

加一个对 NaN 的特判,就可以判断 number 类型:

js
const isNumber = x => typeof x === 'number' && !Number.isNaN(x)
1

全局 isNaN 和 Number.isNaN()

和全局函数 isNaN() 相比,Number.isNaN() 不会自行将参数转换成数字,只有在参数是值为 NaN 的数字时,才会返回 true。

(2) 不能判断 null

js
typeof null // 'object'
1

“历史遗留原因”

JS 中的 typeof null 为 'object',最初就是这样的。在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null 也因此返回 "object"。——MDN typeof null

  1. 可以用于判断 function

function 是指还是 object 类型,只是 typeof 实现了对其的判断,返回 'function'

js
typeof function(){}  // 'function',函数声明
typeof function*(){} // 'function',生成器
typeof (() => {})    // 'function',箭头函数
typeof Array.from    // 'function'
1
2
3
4
  1. 其他对象都返回 'object'
js
const map = new Map()
const set = new Set()
cosnt array = [1, 2, 3]
typeof map // 'object'
typeof set // 'object'
typeof array // 'object'
...
1
2
3
4
5
6
7

对于 typeof x,若 x 是 new Xxx() (构造函数)的,则返回 'object';若是直接 Xxx() 基本包装类型转来的,则 typeof 返回其原本的类型字符串。

js
typeof Number(111) === 'number'
typeof Strig('abc') === 'string'
typeof Boolean(true) === 'boolean'
typeof BigInt(111n) === 'bigint'

typeof new Number(111) === 'object'
typeof new Strig('abc') === 'object'
typeof new Boolean(false) === 'object'
typeof new BigInt(111n) === 'object'
1
2
3
4
5
6
7
8
9

2. instanceof

2.1 作用

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链(一般浏览器实现是 __proto__)上。

2.2 底层原理

对于 L instanceof R,不断循环检查 L.__proto__.__proto__..,直到找到与 R.prototype 相等的,找到则返回 true,直到左边为 null 还没找到,则返回 false。

例 2.1

js
class Animal {}
const animal = new Animal()
animal instanceof Animal // true

const array = new Array(1, 2)
array instanceof Array // true
1
2
3
4
5
6

例 2.2 根据上述规则,基本类型字面量和包装类型都不能使用 instanceof 判断

js
100 instanceof Number // false
'abc' instanceof String // false
true instanceof Boolean // false

Number(100) instanceof Number // false
String('abc') instanceof String // false
Boolean(true) instanceof Boolean // false
1
2
3
4
5
6
7

但是通过构造函数 new Xxx(x) 的可以

js
new Number(100) instanceof Number // false
1

2.3 手写实现一个 instanceof

知道了其底层原理,我们自己实现也不难了。

js
function _instanceof(L, R) {
  while (L.__proto__) {
    if (L.__proto__ === R.prototype) {
      return true
    }
    L = L.__proto__
  }
  return false
}
1
2
3
4
5
6
7
8
9

DANGER

Object.prototype.__proto__ 已经从 Web 标准中删除,虽然一些浏览器目前仍然支持它,但也许会在未来的某个时间停止支持,请尽量不要使用该特性。推荐现在使用 Object.getPrototypeOf() 代替。

所以上述代码使用新标准可以改为:

js
function _instanceof(L, R) {
  let T = Object.getPrototypeOf(L)
  while (T) {
    if (T === R.prototype) {
      return true
    }
    T = Object.getPrototypeOf(T)
  }
  return false
}
1
2
3
4
5
6
7
8
9
10

注意

改写后的 _instanceof 支持 _instanceof(10, Number) 返回 true,但实际上 10 instanceof Number 返回的是 false。这里似乎说明我们的分析错误了,但其实不是,后面会解释。

测试:

js
_instanceof(Function, Object) // true
_instanceof(Object, Function) // true  
_instanceof(Object, String)   // false 
_instanceof(String, Object)   // true
String instanceof Object      // true
1
2
3
4
5

还有一种 isPrototypeOf 方案,和内置的 instanceof 接近:

js
// 在 L 的原型链上查找是否存在 R.prototype
function _instanceof(L, R) {
  return R.prototype.isPrototypeOf(L)
}
1
2
3
4

使用 isPrototypeOf 方法后,_instanceof(10, Number) 返回 false,和 10 instanceof Number 一致了。

2.4 疑问解答

问题1:

js
Function instanceof Object;//true
Object instanceof Function;//true
Function instanceof Function; //true
1
2
3

解释:

js
Function.__proto__.__proto__===Object.prototype // true
Object.__proto__===Function.prototype // true
Function.__proto__===Function.prototype // true
1
2
3

问题2:按照之前的原理分析,下面这两个似乎矛盾了,底层原理好像并不是这样的?

js
console.log( (10).__proto__ === Number.prototype ) // true
console.log( _instanceof(10, Number ) )             // true
console.log( 10 instanceof Number )                // false
1
2
3

本来这个问题我也没想明白,但原理和算法是没错的。在 StackOverflow 提问,才知道原来 JS 中有一个 autoboxing(自动装箱),对于一个 primitive 基本类型,本来没有的一些属性和方法,也能访问到。例如 'aaa' 字符串也能使用 split() 等方法,就是因为被 JS 底层 autoboxing 为 new String(3) 了。

回到我们写的 _instanceof,对于 L.__proto__,当 L 为数字类型,找不到 __proto__ 属性时,会 autoboxing 到 Number 对象上面找,于是永远可以找到一个 __proto__ 等于 R.prototype

说明

按照 ECMA TC39 规范,对于 _instanceof(L, R),当 L 为原始类型,直接返回 false。所以使用 isPrototypeOf 方案才是最接近内置 instanceof 的。当 R 非对象类型,则抛出错误。

经过上述分析,我们可以写出一个更完美的 _instanceof

js
function _instanceof(L, R) {
  if (typeof R !== 'object' || R === null) {
    throw new TypeError('R is no a object') 
  }
  if (typeof L !== 'object' || L === null) {
    return false
  }
  while (L.__proto__) {
    if (L.__proto__ === R.prototype) {
      return true
    }
    L = L.__proto__
  }
  return false
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

参考:

3. 判断数组类型

先考虑 typeof,由于其底层设计,显然是不能来判断数组:

js
typeof [1, 2] // 'object'
typeof null   // 'object'
typeof {a: 1} // 'object' 
1
2
3

3.1 instanceof

由于 array.__proto__ 属性可改,因此存在问题:

js
function isArray1(obj) {
  return obj instanceof Array
}

// 存在问题
let a = [1, 2, 3]
a.__proto__ = {a: 1}
console.log(isArray1(a)) // false
1
2
3
4
5
6
7
8

3.2 constructor

由于 Array.prototype.constructor 属性可改,因此存在问题

js
function isArray2(obj) {
  return obj.constructor === Array
}

// 存在问题
Array.prototype.constructor = {a:1}
console.log(isArray2([1, 2, 3])) // false
1
2
3
4
5
6
7

3.3 Object.prototype.toString.call()

最安全做法,可用来检测其他数据类型。

js
function _isArray(obj) {
  return Object.prototype.toString.call(obj) === '[object Array]'
}
1
2
3

3.4 Array.isArray

使用 ES5 的 Array.isArray() 方法精准判断数组类型。

3.5 兼容 ES5 以前的版本

js
if (!Array.isArray) {
  Array.isArray = function(arg) {
    return Object.prototype.toString.call(arg) === '[object Array]'
  }
}
1
2
3
4
5
js

1

4. 判断原始类型(primitive)

primitive 即基本数据类型,在 JS 中一共有 7 种 primitive,分别是:string、number、boolean、null、undefined、symbol、bigint。

js
function isPrimitive(x) {
  return x !== Object(x)
}
1
2
3

文档解释:MDN Object()

5. 判断对象类型

这里的对象是广义的,包括 plain object、array、function、Set、Map、ArrayLike 等等这些,其实就是除了 7 大基本类型之外的类型。

js
function isObject(x) {
  return typeof x === 'object' && x !== null
}
1
2
3

6. 判断其他类型

除了以上这些类型,有时候需要精确判断某一种类型,比如 symbol、bigint、Map、Set 等等,就可以使用 Object.prototype.toString.call() 方法,返回的是 [Object Xxxx] 的形式。例如 Object.prototype.toString.call(Symbol(1)) 返回的是 '[object Symbol]'

js
function getTypeOf(x) {
  if (Number.isNaN(x)) return 'nan'
  return Object.prototype.toString.call(x)
    .match(/[A-Z].+[a-z]/)[0]
    .toLowerCase()
}

getTypeOf(123)         // number
getTypeOf('abc')       // string
getTypeOf(false)       // boolean
getTypeOf(NaN)         // nan
getTypeOf(null)        // null
getTypeOf(undefined)   // undefined
getTypeOf(123n)        // bigint
getTypeOf(Symbol(123)) // symbol
getTypeOf(new Map())   // map
getTypeOf(new Set())   // set
getTypeOf([1, 2])      // array
getTypeOf({})          // object
getTypeOf(/^123/)      // regexp
getTypeOf(document.querySelectorAll('div')) // nodelist
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

TIP

Object.prototype.toString.call(x) 也可以用 Reflect.toString.call(x) 代替。