Appearance
25. 深入类型判断
0. 前言
提到类型判断,回忆下 JS 中的 8 种数据类型:
- 7 种基本数据类型:
- string
- number
- boolean
- null
- undefined
- symbol
- bigint
- 1 种对象类型:
- object:plain object、array、function、arraylike、Map、Set...
众所周知, JS 中是没有静态类型的,但有时候有需要对变量的类型进行判断,于是 JS 内置了 typeof
、instanceof
等方法来对类型进行判断,但坑不少,对于新手很容易跳进去。
例如,下面这几个结果可能会让你感到诡异:
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
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 类型。
- 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
2
3
4
5
- 不能判断其他 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
- 可以用于判断 function
function 是指还是 object 类型,只是 typeof 实现了对其的判断,返回 'function'
。
js
typeof function(){} // 'function',函数声明
typeof function*(){} // 'function',生成器
typeof (() => {}) // 'function',箭头函数
typeof Array.from // 'function'
1
2
3
4
2
3
4
- 其他对象都返回
'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
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
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
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
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
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
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
2
3
4
5
还有一种 isPrototypeOf 方案,和内置的 instanceof
接近:
js
// 在 L 的原型链上查找是否存在 R.prototype
function _instanceof(L, R) {
return R.prototype.isPrototypeOf(L)
}
1
2
3
4
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
2
3
解释:
js
Function.__proto__.__proto__===Object.prototype // true
Object.__proto__===Function.prototype // true
Function.__proto__===Function.prototype // true
1
2
3
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
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
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
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
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
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
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
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
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
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
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)
代替。