前言
数据类型检测在开发中有非常广泛的应用,常见的有四种方法,每种方法都有优缺点和使用场景。
- typeof
- instanceof & constructor
- Object.prototype.toString.call([value])
数据类型
js 中数据类型分为基本类型和引用类型,基本类型有六种:
number
string
boolean
null
undefined
symbol
(es6)
引用类型包括对象object
、数组array
、函数function
等,统称对象类型:
object
typeof
我们最常用的莫过于 typeof,typeof 是一元操作符,放在其单个操作数的前面,操作数可以是任意类型。返回值为表示操作数类型的一个字符串。
特点
在检测基本类型值(null除外)和函数类型值的时候很方便。
在基本类型中,null返回了object。
引用类型中,除了函数外(如 Array、Function、Date、RegExp、Error 等)其他返回的object。
typeof返回值都是小写的字符串,如下。
- undefined
- object
- boolean
- number
- string
- object
场景
形参赋值默认值
function func(n,m){
n === undefined ? n=0:null;
typeof m === 'undefined' ? m=0:null;
// 当 n=false 基于逻辑或和逻辑与 会有问题
n=n||0
// ES 2020 新增 空位合并运算符 ??
//表示我们仅在第一项为 null 或 undefined 时设置默认值
m = m ?? 0
}
instanceof
用来检测实例是否属于某个类的运算符
特点
不能处理基本数据类型
let arr =[],reg = /^$/;
arr instanceof Array // true
reg instanceof Array // false
let n=12,m= new Number('12') // 分别是字面量和构造函数创建
n instanceof Array // false
m instanceof Array // true
不能正确处理继承中的类
只要在当前实例原型链中(proto)出现过的类,检测结果都是true。如果修改原型链或者检测预先类都会出现一点问题。
let arr =[];
arr instanceof Object // false
constructor
特点
在类的原型上一般都有constructor属性,存储当前类的本身,利用这一点,验证是否为所属类,从而进行类型判断。
但是constructor的值太容易被修改了。
let n = 12,arr=[];
n.constructor === Number // true
arr.constrctor === Array //true
arr.constrctor === Object //false
arr.constrctor = 11 // Func.prototype={} 或者修改原型链
arr.constrctor === Array //false
Obejct.prototype.toString.call()
首先来看英文版的定义:
当 toString 方法被调用的时候,下面的步骤会被执行:
- 如果 this 值是 undefined,就返回 [object Undefined]
- 如果 this 的值是 null,就返回 [object Null]
- 让 O 成为 ToObject(this) 的结果
- 让 class 成为 O 的内部属性 [[Class]] 的值
- 最后返回由 "[object " 和 class 和 "]" 三个部分组成的字符串
特点
调用Object原型上的方法时,方法中的this是要检测的数据类型,结果会返回一个由 "[object " 和 class 和 "]" 组成的字符串,而 class 是要判断的对象的内部属性。
这个方法很强大,能检测出几乎所有的基本类型和引用类型,但是无法检测自定义的类。
console.log([12,34].toString()) // "[12,34]"
所以我们可以识别至少 14 种类型。
// 以下是11种:
var number = 1; // [object Number]
var string = '123'; // [object String]
var boolean = true; // [object Boolean]
var und = undefined; // [object Undefined]
var nul = null; // [object Null]
var obj = {a: 1} // [object Object]
var array = [1, 2, 3]; // [object Array]
var date = newDate(); // [object Date]
var error = newError(); // [object Error]
var reg = /a/g; // [object RegExp]
var func = functiona(){}; // [object Function]
functioncheckType() {
for (var i = 0; i < arguments.length; i++) {
console.log(Object.prototype.toString.call(arguments[i]))
}
}
checkType(number, string, boolean, und, nul, obj, array, date, error, reg, func)
除了以上 11 种之外,还有:
console.log(Object.prototype.toString.call(Math)); // [object Math]
console.log(Object.prototype.toString.call(JSON)); // [object JSON]
functiona() {
console.log(Object.prototype.toString.call(arguments)); // [object Arguments]
}
a();
无法检测自定义的类
class myCat{}
Object.prototype.toString.call(myCat) // "[object Function]"
数据类型检测方法封装
简单的封装
function isType(obj, type) {
return Object.prototype.toString.call(obj).includes(type);
}
let a = true;
console.log(isType(a, 'Boolean'));
console.log(isType(a, 'Object'));
使用高阶函数
虽然上面的函数能实现数据类型检测,但是手动书写字符串容易造成问题,接着使用高阶函数改造。
考虑到实际情况下并不会检测 Math 和 JSON,所以去掉这两个类型的检测。
// 函数柯里化:函数参数只有一个
function isType(type) {
return function(obj) {
return Object.prototype.toString.call(obj).includes(type);
};
}
let types = [
'Boolean',
'Number',
'String',
'Function',
'Array',
'Date',
'RegExp',
'Object',
'Error',
'Null',
'Undefined'
];
let fns = {};
types.forEach((type) => {
fns['is' + type] = isType(type);
});
console.log(fns.isBoolean(a));
提升性能
Object.prototype.toString.call
性能不如 typeof,但胜在可通用。如果是基本类型,就使用 typeof,引用类型就使用 toString。
function isType(type) {
return function(obj) {
let res = typeof obj;
return res === 'object' || res === 'function'
? Object.prototype.toString.call(obj).toLowerCase().includes(type)
: res.includes(type);
};
}
let types = [
'boolean',
'number',
'string',
'function',
'array',
'date',
'regExp',
'object',
'error',
'null',
'undefined'
];
let fns = {};
types.forEach((type) => {
fns['is' + type[0].toUpperCase() + type.substr(1)] = isType(type);
});
console.log(fns.isBoolean(true));
console.log(fns.isBoolean([]));
返回数据类型方法封装
方法封装
此外鉴于 typeof 的结果是小写,我也希望所有的结果都是小写。
// 第一版
var class2type = {};
// 生成class2type映射
"Boolean Number String Function Array Date RegExp Object Error Null Undefined".split(" ").map(function(item, index) {
class2type["[object " + item + "]"] = item.toLowerCase();
})
function type(obj) {
return typeof obj === "object" || typeof obj === "function" ?
class2type[Object.prototype.toString.call(obj)] || "object" :
typeof obj;
}
在 IE6 中,null 和 undefined 会被 Object.prototype.toString 识别成 [object Object]。
// 第二版
var class2type = {};
// 生成class2type映射
"Boolean Number String Function Array Date RegExp Object Error".split(" ").map(function(item, index) {
class2type["[object " + item + "]"] = item.toLowerCase();
})
function type(obj) {
// 一箭双雕
if (obj == null) {
return obj + "";
}
return typeof obj === "object" || typeof obj === "function" ?
class2type[Object.prototype.toString.call(obj)] || "object" :
typeof obj;
}
类型检测
有了 type 函数后,我们可以对常用的判断直接封装,比如 isFunction:
function isFunction(obj) {
return type(obj) === "function";
}
jQuery 判断数组类型,旧版本是通过判断 Array.isArray 方法是否存在,如果存在就使用该方法,不存在就使用 type 函数。
var isArray = Array.isArray || function( obj ) {
return type(obj) === "array";
}
但是在 jQuery v3.0 中已经完全采用了 Array.isArray。
其他类型检测
plainObject
plainObject 来自于 jQuery,可以翻译成纯粹的对象,所谓"纯粹的对象",就是该对象是通过 "{}" 或 "new Object" 创建的,该对象含有零个或者多个键值对。除了 {} 和 new Object 创建的之外,jQuery 认为一个没有原型的对象也是一个纯粹的对象。
实际上随着 jQuery 版本的提升,isPlainObject 的实现也在变化,我们今天讲的是 3.0 版本下的 isPlainObject,我们直接看源码。
// 上节中写 type 函数时,用来存放 toString 映射结果的对象
var class2type = {};
// 相当于 Object.prototype.toString
var toString = class2type.toString;
// 相当于 Object.prototype.hasOwnProperty
var hasOwn = class2type.hasOwnProperty;
function isPlainObject(obj) {
var proto, Ctor;
// 排除掉明显不是obj的以及一些宿主对象如Window
if (!obj || toString.call(obj) !== "[object Object]") {
return false;
}
/**
* getPrototypeOf es5 方法,获取 obj 的原型
* 以 new Object 创建的对象为例的话
* obj.__proto__ === Object.prototype
*/
proto = Object.getPrototypeOf(obj);
// 没有原型的对象是纯粹的,Object.create(null) 就在这里返回 true
if (!proto) {
return true;
}
/**
* 以下判断通过 new Object 方式创建的对象
* 判断 proto 是否有 constructor 属性,如果有就让 Ctor 的值为 proto.constructor
* 如果是 Object 函数创建的对象,Ctor 在这里就等于 Object 构造函数
*/
Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
// 在这里判断 Ctor 构造函数是不是 Object 构造函数,用于区分自定义构造函数和 Object 构造函数
return typeof Ctor === "function"
&& hasOwn.toString.call(Ctor) === hasOwn.toString.call(Object);
}
console.log(hasOwn.toString.call(Ctor)); // function Object() { [native code] }
console.log(Object.prototype.toString.call(Ctor)); // [object Function]
发现返回的值并不一样,这是因为 hasOwn.toString 调用的其实是 Function.prototype.toString,而且 Function 对象覆盖了从 Object 继承来的 Object.prototype.toString 方法。
函数的 toString 方法会返回一个表示函数源代码的字符串。具体来说,包括 function关键字,形参列表,大括号,以及函数体中的内容。
EmptyObject
jQuery提供了 isEmptyObject 方法来判断是否是空对象,代码简单,我们直接看源码:
functionisEmptyObject( obj ) {
var name;
for ( name in obj ) {
return false;
}
return true;
}
isEmptyObject 就是判断是否有属性,for 循环一旦执行,就说明有属性,有属性就会返回 false。
但是根据这个源码我们可以看出isEmptyObject实际上判断的并不仅仅是空对象。
console.log(isEmptyObject({})); // true
console.log(isEmptyObject([])); // true
console.log(isEmptyObject(null)); // true
console.log(isEmptyObject(undefined)); // true
console.log(isEmptyObject(1)); // true
console.log(isEmptyObject('')); // true
console.log(isEmptyObject(true)); // true
jQuery可能是因为考虑到实际开发中 isEmptyObject 用来判断 {} 和 {a: 1} 已经足够,如果真的是只判断 {},完全可以使用封装的type函数筛选掉不适合的情况。
Window对象
Window 对象作为客户端 JavaScript 的全局对象,它有一个 window 属性指向自身。我们可以利用这个特性判断是否是 Window 对象。
function isWindow( obj ) {
return obj != null && obj === obj.window;
}
isArrayLike
isArrayLike,看名字可能会让我们觉得这是判断类数组对象的,其实不仅仅是这样,jQuery 实现的 isArrayLike,数组和类数组都会返回 true。
function isArrayLike(obj) {
// obj 必须有 length属性
var length = !!obj && "length"in obj && obj.length;
var typeRes = type(obj);
// 排除掉函数和 Window 对象
if (typeRes === "function" || isWindow(obj)) {
return false;
}
return typeRes === "array" || length === 0 ||
typeof length === "number" && length > 0 && (length - 1) in obj;
}
重点分析 return 这一行,使用了或语句,只要一个为 true,结果就返回 true。
所以如果 isArrayLike 返回true,至少要满足三个条件之一:
- 是数组
- 长度为 0
- lengths 属性是大于 0 的数组,并且obj[length - 1]必须存在
第一个就不说了,看第二个,为什么长度为 0 就可以直接判断为 true 呢?
那我们写个对象:
var obj = {a: 1, b: 2, length: 0}
isArrayLike 函数就会返回 true,那这个合理吗?
回答合不合理之前,我们先看一个例子:
function a(){
console.log(isArrayLike(arguments))
}
a();
如果我们去掉length === 0 这个判断,就会打印 false,然而我们都知道 arguments 是一个类数组对象,这里是应该返回 true 的。
所以是不是为了放过空的 arguments 时也放过了一些存在争议的对象呢?
第三个条件:length 是数字,并且 length > 0 且最后一个元素存在。
为什么仅仅要求最后一个元素存在呢?
让我们先想下数组是不是可以这样写:
var arr = [,,3]
当我们写一个对应的类数组对象就是:
var arrLike = {
2: 3,
length: 3
}
也就是说当我们在数组中用逗号直接跳过的时候,我们认为该元素是不存在的,类数组对象中也就不用写这个元素,但是最后一个元素是一定要写的,要不然 length 的长度就不会是最后一个元素的 key 值加 1。比如数组可以这样写
var arr = [1,,];
console.log(arr.length) // 2
但是类数组对象就只能写成:
var arrLike = {
0: 1,
length: 1
}
所以符合条件的类数组对象是一定存在最后一个元素的!
这就是满足 isArrayLike 的三个条件,其实除了 jQuery 之外,很多库都有对 isArrayLike 的实现,比如 underscore:
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
var isArrayLike = function(collection) {
var length = getLength(collection);
returntypeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};
isElement
isElement 判断是不是 DOM 元素。
var isElement = function(obj) {
return !!(obj && obj.nodeType === 1);
};
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!