web前端高階 - JavaScript常用工具類庫封裝
阿新 • • 發佈:2020-12-24
技術標籤:WEB前端高階教程類庫封裝公共類庫封裝深淺克隆深淺合併檢測純物件資料型別檢測
JavaScript常用工具類庫封裝
- 在我們自己去封裝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":防止在部分瀏覽器中,檢測<object>元素物件結果也是"function"
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;
}
})();