本文系统总结了 JavaScript 中多种数据类型检测方法。深入解析 typeof、instanceof、constructor 及 Object.prototype.toString 的适用场景与局限性,探讨 Array.isArray、Number.isNaN 等 ES6 新增方法的优势,并介绍纯对象、类数组及鸭式辨别的检测技巧,帮助开发者在复杂场景中准确判断数据类型。
背景
众所周知,JavaScript 是一门动态类型的弱类型脚本语言,它采用动态类型系统以及基于原型的继承机制。
由于缺乏静态类型的约束,由数据类型错误引发的程序问题无法在编译阶段被捕获。为了确保代码的健壮性,必须在运行时进行各种检查与兼容性处理,因此,熟练掌握且准确地检测数据类型是掌握这门语言的核心基础之一。
判断数据类型的手段有哪些?
总体而言,主要检测方法包括:typeof、instanceof、Object.prototype.toString、constructor、鸭式辨别的类型检测,以及针对特定类型的专用方法如 Array.isArray() 和 Number.isNaN()。尽管方法众多,但它们各自的应用场景存在显著差异。
1. 使用 typeof 判断基础数据类型:
其返回值包含 undefined、string、number、boolean、object、function 和 symbol 七种类型。

可以看出,typeof 作为官方提供的类型检测操作符,在检测 undefined、string、boolean、symbol 等基本数据类型以及 function 方面表现可靠。然而,其不足之处主要体现在:
1)无法区分具体的对象类型(如 Array、Date、RegExp)。 2)typeof null === 'object' // 结果竟然是 true。。。。
缺陷 2)可以通过在判断对象引用类型时增加 null 检查来规避,即 typeof x === 'object' && x !== null。但无法区分具体对象类型确实是一个主要痛点。
2. 使用 instanceof 判断对象数据类型

该运算符用于检测某个构造函数的 prototype 属性是否出现在目标对象的原型链上。
这是一种基于预判断的检测方式,它不会像 typeof 那样直接返回字符串类型的结果,而是需要预先知道对象类型的构造函数,最终返回一个布尔值。
从命名即可看出其检测规则:判断实例是否由某个构造函数创建。了解原理后,我们可以动手实现一个自定义的 instanceof。
function myInstanceof(target,constructor){
const baseType = ['string', 'number','boolean','undefined','symbol']
if(baseType.includes(typeof(target))) { return false }
//原型链其实就是个对象组成的链表,遍历这个链表,
let prototype = Object.getPrototypeOf(target);
while(prototype){
//一旦链上有对象有符合,就返回true
if(prototype === constructor.prototype){
return true
}else{
prototype = Object.getPrototypeOf(prototype)
}
}
return false
}
console.log(myInstanceof([],Array))
在 JavaScript 中,广义上可以认为万物皆对象。虽然实例是通过构造函数创建的,但构造函数本身只是执行逻辑的机器,实例的公共属性和方法(即灵魂和性格)都共享自构造函数 prototype 属性指向的原型对象。由于原型对象本身也是纯对象,且由 Object 构造函数创建,这会导致如下现象。

对于数组,遍历其原型链时,Array.prototype 和 Object.prototype 都会出现。
此外,instanceof 无法判断通过字面量方式创建的基本数据类型。例如:

如何弥补上述缺陷?答案是在特殊场景下使用 constructor 属性替代 instanceof。
3. 使用 constructor 属性
首先明确,constructor 是原型上的属性,实例继承自原型,因此可以直接在实例上访问该属性。 先观察 constructor 的通用性表现:

表现意外地不错。除了 null 和 undefined,拥有 constructor 属性的基础包装类型或对象类型都能被准确判断。

它能准确区分 Array 和 Object,因为它不像 instanceof 那样遍历整条原型链,仅在实例身上进行判断。但也有一个致命缺陷:实例上的 constructor 属性极易被修改,一旦修改,该方法将失效。
4. 使用 toString 方法
首先,JavaScript 的对象类型或基础类型的包装对象都拥有 toString 方法,该方法继承自 Object.prototype.toString(),调用后会返回对应类型的字符串标记 "[object Type]"。

这种方法有“乱拳打死老师傅”、无心插柳柳成荫的效果。原本它仅用于获取表示对象的字符串,现在用于 JavaScript 类型检测,表现非常出色,对基础类型及对象类型均适用。如果非要挑刺,返回的字符串较为复杂,使用稍显不便,接下来让我们动手简化它。
先编写一个简版:
function isType(type,value){
return Object.prototype.toString.call(value) === `[object ${type}]`
}
console.log(isType('Array',[]))
console.log(isType('Number',1))
这样使用仍不太方便,'Array'、'Number' 这样的类型参数容易拼写错误。因此,希望方法可以预设参数,并构造一个函数工厂,调用后返回类似于 isArray 这样的函数。在 IDE 中,函数名相比字符串能提供更好的代码提示,不易拼写错误。
function isType(type){
return function(value){
return Object.prototype.toString.call(value) === `[object ${type}]`
}
}
const isArray = isType('Array')
const isNumber = isType('Number')
console.log(isArray([]),isNumber(1))
这里运用了高阶函数的思想,保留参数并返回一个新函数。考虑到 JavaScript 中 bind 方法除了绑定 this,也具有保留参数并返回新函数的功能,在此处使用非常合适。
function isType(type,value){
return Object.prototype.toString.call(value) === `[object ${type}]`
}
const isArray = isType.bind(null,'Array')
const isNumber = isType.bind(null,'Number')
console.log(isArray([]),isNumber(1))
更进一步,可以使用参数柯里化的思想进行改造:
function isType(type,value){
return Object.prototype.toString.call(value) === `[object ${type}]`
}
function curring (fn,...args1){
let len = fn.length;
return function(...args2){
const args = args1.concat(args2);
if(args.length < len){
return curring(fn,...args)
}else{
return fn(...args)
}
}
}
const isArray = curring(isType,'Array')
const isNumber = curring(isType,'Number')
console.log(isArray([]),isNumber(1))
最后,丰富支持的类型,大功告成。
const types = [
'Null',
'Undefined',
'String',
'Number',
'Boolean',
'Object',
'Array',
'Date',
'Function',
'RegExp',
'Symbol',
'Math',
]
const checkTypeUtil = {}
types.forEach((type)=>{
checkTypeUtil[`is${type}`] = curring(isType,type)
})
export {
checkTypeUtil
}
console.log(checkTypeUtil.isArray([]))
5. 使用 Array.isArray 判断数组
上文提到 instanceof 可用于检测数组,但在 iframe 创建的多 window 环境中,由于 window 全局环境需要隔离,Array 和 Array.prototype 在每个窗口中必须是不同的,即 iframeA.Array.prototype ≠ iframeB.Array.prototype,因此 iframeA.arr instanceof iframeB.Array 必定返回 false。虽然这是小概率事件,但在 iframe 场景下互相传值时非常可能发生。使用 ES6 提供的 Array.isArray 则无此问题,可准确判断数组。
可以这样进行 polyfill:
if (!Array.isArray) {
Array.isArray = function(x) {
return Object.prototype.toString.call(x) === '[object Array]';
};
}
6. 区分 ArrayLike 与 Array
类数组的定义是:
- 拥有 length 属性,其他属性(索引)为非负整数(对象中的索引会被当作字符串处理)。
- 不具有数组所具有的方法。
function isLikeArray(x){
if(!(typeof x === 'object' && x !== null)){
return false
}
return typeof x.length === 'number' && x.length >= 0 && !Array.isArray(x)
}
类数组可以使用 Array.from 或 Array.prototype.slice.call(val) 转换为真正的数组。
7. 判断一个对象是否是纯对象(或普通对象)
纯对象的定义:特指通过以下三种方式创建的对象:
- new Object
- 对象字面量创建 {}
- Object.create(null)
jquery、lodash 源码均采用以下方法来检测:
const funcToString = Function.prototype.toString
const objectCtorString = funcToString.call(Object)
function isPlainObject(value){
// 先用toString先排除其他数据类型
if(!value || !Object.prototype.toString.call(value) === "[object Object]"){
return false
}
const proto = Object.getPrototypeOf(value)
if(proto === null){//兼容Object.create(null)这样创建的对象
return true
}
const Ctor = Object.prototype.hasOwnProperty.call(proto,'constructor') && proto.constructor;
if(typeof Ctor !== 'function'){
return false
}
// 这里通过字符串判断构造函数是否是Object,而不是直接使用instanceof,是为了避免上边提到的 多window环境Object不同的问题
if(funcToString.call(Ctor) === objectCtorString){
return true
}
return false
}
console.log(isPlainObject(Object.create(null)))
console.log(isPlainObject(new Object))
console.log(isPlainObject({a:1}))
8. NaN 如何检测,Number.isNaN 与 isNaN 有何区别

结论:Number.isNaN 会严格判断传入的值是否直接等于 NaN。
isNaN 则会先进行 Number() 转换,然后再判断是否是 NaN。
9. 鸭式类型检测法
其实上文利用 constructor 判断数据类型,就是采用了这种方法。判断一个动物是不是鸭子,通过看起来像鸭子、叫起来像鸭子这样的经验判断即可大致确定。
例如,判断一个对象是否是 Promise,可以这样操作:
function isPromise(x){
if(!(x instanceof Promise)){
return false
}
return typeof x.then === 'function'
}
总结
到此这篇关于 JS 数据类型检测的文章就介绍到这了,更多相关 JS 数据类型检测内容请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!