- 在我们自己去封装JavaScript组件时,难免会用到一些判断的逻辑处理,比如判断一个值是否是空对象,是否是一个纯对象,是否是一个类数组等等,每次判断都要写一堆逻辑,用起来很麻烦。下面就将常用的判断逻辑和一些常用的方法进行封装成一个公共类库,以后再使用时就可以直接拿来用了。
- 本次封装参考jquery3.5.1版本源码
- 该类库主包括如下方法的封装:
- 数据类型检测 toType
- 函数检测 isFunction
- window对象检测 isWindow
- 类数组检测 isArrayLike
- 纯对象检测 isPlainObject
- 空对象检测 isEmptyObject
- 数字类型检测 isNumeric
- 数组或对象遍历方法封装 each
- 深克隆方法封装 deepClone
- 浅克隆方法封装 shallowClone
- 深合并方法封装 deepMerge
- 浅合并方法封装 shallowMerge
最后将这些方法暴露到全局对象上,话不多说,直接上代码了
//公共方法封装
(function(){
//创建一个空对象
var class2type = {};
//用来检测数据类型
var toString = class2type.toString;//Object.prototype.toString
//用来检测是否是私有属性
var hasOwn = class2type.hasOwnProperty;//Object.prototype.hasOwnProperty
//Object.prototype.hasOwnProperty是一个函数,那么hasOwn肯定也是一个函数,每个函数都是Function的实例
//所以hasOwn.toString 就是Function.prototype.toString
//用来把函数转换为字符串
var fnToString = hasOwn.toString;//Function.prototype.toString
//相当于Function.prototype.toString.call(Object) => 将toString中的this改为Object
//相当于Object.toString(); 注意这里不是Object.prototype.toString()
//把Object变成字符串
var ObjectFunctionString = fnToString.call(Object);"function Object(){...}"
//获取当前对象的原型链__proto__
var getProto = Object.getPrototypeOf;
//在jQuery源码中有这样一段代码:现将字符串切割为数组,然后遍历这个数组给class2type添加以object+数组中的项为名的属性,属性值则是数组中的每一项的小写形式
/*
jQuery.each("Boolean Number String Function Array Date RegExp Object Error Symbol".split(" "), function(_i, name){
class2type["[object "+name+"]"] = name.toLowerCase();
});
*/
//上面代码我们可以转换为:
//建立数据类型检测映射表
var mapType = ["Boolean", "Number", "String", "Function", "Array", "Date", "RegExp", "Object", "Error", "Symbol"];
mapType.forEach(function(name){
class2type["[object "+name+"]"] = name.toLowerCase();
});
//最后class2type将变为如下所示:
/*
class2type={
"[object Boolean]":"boolean",
"[object Number]":"number",
"[object String]":"string"
}
*/
//封装万能的数据类型检测方法
function toType(obj){
//如果obj是null或undefined(undefined == null)
//则返回字符串null或字符串undefined
if(obj == null) {
return obj + "";//obj和字符串相加结果转换为字符串
};
//基于字面量方式创建的基本数据类型,直接基于typeof检测即可(性能稍高一些)
//剩余的则基于Object.prototype.toString.call的方式来检测,把获取的值拿到映射表中进行匹配,
//得到的值是字符串对应的数据类型
return typeof obj === "object" || typeof obj === "function" ?
class2type[toString.call(obj)] || "object" : typeof obj;
}
//检测是否为函数
var isFunction = function isFunction(obj){
//元素节点[DOM对象]具备nodeType(元素、文本、注释、document 对应1、3、8、9)
//typeof obj.nodeType !== "number":防止在部分浏览器中,检测
return typeof obj === "function" && typeof obj.nodeType !== "number";
}
//检测是否是window对象
var isWindow = function isWindow(obj){
//window对象的特点:window.window === window
return obj != null && obj === obj.window;
}
//检测是否为数组或类数组
var isArrayLike = function isArrayLike(obj){
//!!obj将obj转换为布尔类型
//如果!!obj为true并且"length" in obj 为true则获取obj的length值
//所以length存储的是对象的length属性或false
//type存储的是检测的数据类型
var length = !!obj && "length" in obj && obj.length,
type = toType(obj);
//window.length = 0 windows有length属性
//Function.prototype.length = 0 Function的原型也有length属性
//所以这里是排除window和Function,有length属性不一定就是数组
if(isFunction(obj) || isWinow(obj)){
return false;
}
//type === "array"说明是数组
//length === 空的类数组
//最后一个条件用于判断是非空数组:有length属性并且length是一个数字类型,length的值大于0,并且最大索引在数组中
//length - 1就是最大索引
return type === "array" || length === 0 ||
typeof length === "number" && length > 0 && (length - 1) in obj;
//逻辑与的优先级大于逻辑或
}
//检测是否是纯对象,例如:{}
var isPlainObject = function isPlainObject(object){
var proto, Ctor;
//不存在,或基于toString检测的结果都不是[object Object]则一定不是对象
if(!obj || toString.call(obj) !== "[object Object]"){
return false;
}
//获取当前值的原型链(直属类的原型链)
proto = getProto(obj);
//通过Object.create(null)创建的对象是没有__proto__ 的,所以肯定是纯对象
if(!proto){
return true;
}
//Ctor 存储原型对象上的constructor属性,如果没有这个属性就是false
Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
//如果构造函数是一个函数,
//条件成立说明原型上的构造函数是Object: obj就是Object的一个实例,并且obj.__proto__ === Object.prototype
return typeof Ctor === "function" && fnToString.call(Ctor) === ObjectFunctionString;
//fnToString.call = Object.prototype.hasOwnProperty.toString.call()
//ObjectFunctionString = fnToString.call(Object)这句代码在上面已经定义,是将Object转换为字符串
//而fnToString.call(Ctor)是将Ctor转换为字符串,如果二者相等则条件成立
}
//检测是否为空对象
var isEmptyObject = function isEmptyObject(obj){
//jQuery的原生写法,但是优缺点:基于for in循环有很多问题,无法获取Symbol类型的属性,会把自己定义在原型上的属性也获取到等等
/*
var name;
for(name in obj){
return false;
}
return true;
*/
//排除null或undefined
if(obj == null) return false;
if(typeof !== "object") return false;
//是一个对象,纯对象或特殊对象都可以
var keys = Object.keys(obj);
//如果兼容再去拼接
if(hasOwn.call(Object,"getOwnPropertySymbols")){
keys.concat(Object.getOwnPropertySymbols(obj));
}
return keys.length === 0;
}
//检测是否为数字
var isNumeric = function isNumeric(obj){
var type = toType(obj);
//纯数字或是数字的字符串形式("10") 并且不是NaN
//obj - parseFloat(obj)只要有任何一个值不是数字,结果都是NaN, 可以直接用+obj代替
return (type === "number" || type === "string") && !isNaN(obj - parseFloat(obj));
}
var each = function each(obj, callback){
var length, i = 0;
//数组或类数组处理
if(isArrayLike(obj)){
length = obj.length;
for(; i < lenght; i++){
if(callback.call(obj[i], i, obj[i]) === false){
break;
}
}
}else{//对象处理
var keys = Object.keys(obj);//获取所有私有非Symbol类型的key
//如果支持Symbol类型,则把Symbol类型的属性添加到keys中
typeof Symbol !== "undefined" ? keys = keys.concat(Object.getOwnPropertySymbols(obj)) : null;
for(; i < keys.length; i++){
var key = keys[i];
if(callback.call(obj[key], key, obj[key]) === false){
break;
}
}
}
return obj;
}
//定义一个获取对象/数组所有私有属性(包括Symbol类型)的方法
function getOwnProperties(obj){
//数据类型检测
if(obj == null) return [];
//获取所有私有属性包括Symbol类型
let keys = [
...Object.keys(obj),
...Object.getOwnPropertySymbols(obj)
]
return keys;
}
var shallowClone = function shallowClone(obj){
//如果是基本数据类型值,则传啥就返回啥
let type = toType(obj);
if(/^(number|string|boolean|null|undefined|symbol|bigint)$/.test(type)) return obj;
if(type === 'function') {
//返回一个不同函数,但最后执行效果跟原始函数一致
return function proxy(){
obj();
}
}
//if(type === 'regexp') return new RegExp(obj);
//if(type === 'date') return new Date(obj);
if(/^(regexp|date)$/.test(type)) return new obj.constructor(obj);
if(type === 'error') return new Error(obj.message);
let keys = getOwnProperties(obj);
let clone = {};
Array.isArray(obj) ? clone = [] : null;
keys.forEach(item => {
clone[item] = obj[item];
});
return clone;
}
var deepClone = function deepClone(obj, cache = new Set()){
let type = toType(obj);
//如果不是数组或对象则直接按浅克隆处理
if(!/^(array|object)$/.test(type)) return shallowClone(obj);
let keys = getOwnProperties(obj);
let clone = {};
Array.isArray(obj) ? clone = [] : null;
if(cache.has(obj)) return obj;
cache.add(obj);
keys.forEach(item => {
clone[item] = deepClone(obj[item], cache);
});
return clone;
}
var shallowMerge = function shallowMerge(obj1, obj2){
var isPlain1 = isPlainObject(obj1);
var isPlain2 = isPlainObject(obj2);
//只要obj1不是对象,那么不管obj2是不是对象,都用obj2直接替换obj1
if(!isPlain1) return obj2;
//走到这一步时,说明obj1肯定是对象,那如果obj2不是对象,则还是以obj1为主
if(!isPlain2) return obj1;
//如果上面两个条件都不成立,那说明obj1和obj2肯定都是对象, 则遍历obj2 进行合并
let keys = [
...Object.keys(obj2),
...Object.getOwnPropertySymbols(obj2)
]
keys.forEach(function(key){
obj1[key] = obj2[key];
});
return obj1;
}
var deepMerge = function deepMerge(obj1, obj2){
var isPlain1 = isPlainObject(obj1);
var isPlain2 = isPlainObject(obj2);
//obj1或obj2中只要其中一个不是对象,则按照浅合并的规则进行合并
if(!isPlain1 || !isPlain2) return shallowMerge(obj1, obj2);
//如果都是对象,则进行每一层级的递归合并
let keys = [
...Object.keys(obj2),
...Object.getOwnPropertySymbols(obj2)
]
keys.forEach(function(key){
obj1[key] = deepMerge(obj1[key], obj2[key]);//这里递归调用
});
return obj1;
}
var utils = {
toType :toType ,
isFunction:isFunction,
isWindow:isWindow ,
isArrayLike:isArrayLike ,
isPlainObject :isPlainObject ,
isEmptyObject:isEmptyObject ,
isNumeric:isNumeric,
each:each,
shallowClone: shallowClone ,
deepClone: deepClone,
shallowMerge: shallowMerge,
deepMerge: deepMerge
}
//挂载到全局
//浏览器环境
if(typeof window !== 'undefined'){
window._ = window.utils = utils;
}
//node 环境
if(typeof module === 'object' && typeof module.exports === 'object'){
module.exports.utils = utils;
}
})();