從零實現jQuery的extend
前言
jQuery 的 extend 是 jQuery 中應用非常多的一個函數,今天我們一邊看 jQuery 的 extend 的特性,一邊實現一個 extend!
extend 基本用法
先來看看 extend 的功能,引用 jQuery 官網:
Merge the contents of two or more objects together into the first object.
翻譯過來就是,合並兩個或者更多的對象的內容到第一個對象中。
讓我們看看 extend 的用法:
jQuery.extend( target [, object1 ] [, objectN ] )
第一個參數 target,表示要拓展的目標,我們就稱它為目標對象吧。
後面的參數,都傳入對象,內容都會復制到目標對象中,我們就稱它們為待復制對象吧。
舉個例子:
var obj1 = {
a: 1,
b: { b1: 1, b2: 2 }
};
var obj2 = {
b: { b1: 3, b3: 4 },
c: 3
};
var obj3 = {
d: 4
}
console.log($.extend(obj1, obj2, obj3));
// {
// a: 1,
// b: { b1: 3, b3: 4 },
// c: 3,
// d: 4
// }
當兩個對象出現相同字段的時候,後者會覆蓋前者,而不會進行深層次的覆蓋。
extend 第一版
結合著上篇寫得 《JavaScript專題之深淺拷貝》,我們嘗試著自己寫一個 extend 函數:
// 第一版
function extend() {
var name, options, copy;
var length = arguments.length;
var i = 1;
var target = arguments[0];
for (; i < length; i++) {
options = arguments[i];
if (options != null) {
for (name in options) {
copy = options[name];
if (copy !== undefined){
target[name] = copy;
}
}
}
}
return target;
};
extend 深拷貝
那如何進行深層次的復制呢?jQuery v1.1.4 加入了一個新的用法:
jQuery.extend( [deep], target, object1 [, objectN ] )
也就是說,函數的第一個參數可以傳一個布爾值,如果為 true,我們就會進行深拷貝,false 依然當做淺拷貝,這個時候,target 就往後移動到第二個參數。
還是舉這個例子:
var obj1 = {
a: 1,
b: { b1: 1, b2: 2 }
};
var obj2 = {
b: { b1: 3, b3: 4 },
c: 3
};
var obj3 = {
d: 4
}
console.log($.extend(true, obj1, obj2, obj3));
// {
// a: 1,
// b: { b1: 3, b2: 2, b3: 4 },
// c: 3,
// d: 4
// }
因為采用了深拷貝,會遍歷到更深的層次進行添加和覆蓋。
extend 第二版
我們來實現深拷貝的功能,值得註意的是:
- 需要根據第一個參數的類型,確定 target 和要合並的對象的下標起始值。
- 如果是深拷貝,根據 copy 的類型遞歸 extend。
// 第二版
function extend() {
// 默認不進行深拷貝
var deep = false;
var name, options, src, copy;
var length = arguments.length;
// 記錄要復制的對象的下標
var i = 1;
// 第一個參數不傳布爾值的情況下,target默認是第一個參數
var target = arguments[0] || {};
// 如果第一個參數是布爾值,第二個參數是才是target
if (typeof target == ‘boolean‘) {
deep = target;
target = arguments[i] || {};
i++;
}
// 如果target不是對象,我們是無法進行復制的,所以設為{}
if (typeof target !== ‘object‘) {
target = {}
}
// 循環遍歷要復制的對象們
for (; i < length; i++) {
// 獲取當前對象
options = arguments[i];
// 要求不能為空 避免extend(a,,b)這種情況
if (options != null) {
for (name in options) {
// 目標屬性值
src = target[name];
// 要復制的對象的屬性值
copy = options[name];
if (deep && copy && typeof copy == ‘object‘) {
// 遞歸調用
target[name] = extend(deep, src, copy);
}
else if (copy !== undefined){
target[name] = copy;
}
}
}
}
return target;
};
在實現上,核心的部分還是跟上篇實現的深淺拷貝函數一致,如果要復制的對象的屬性值是一個對象,就遞歸調用 extend。不過 extend 的實現中,多了很多細節上的判斷,比如第一個參數是否是布爾值,target 是否是一個對象,不傳參數時的默認值等。
接下來,我們看幾個 jQuery 的 extend 使用效果:
target 是函數
在我們的實現中,typeof target
必須等於 object
,我們才會在這個 target
基礎上進行拓展,然而我們用 typeof
判斷一個函數時,會返回function
,也就是說,我們無法在一個函數上進行拓展!
什麽,我們還能在一個函數上進行拓展!!
當然啦,畢竟函數也是一種對象嘛,讓我們看個例子:
function a() {}
a.target = ‘b‘;
console.log(a.target); // b
實際上,在 underscore 的實現中,underscore 的各種方法便是掛在了函數上!
所以在這裏我們還要判斷是不是函數,這時候我們便可以使用《JavaScript專題之類型判斷(上)》中寫得 isFunction 函數
我們這樣修改:
if (typeof target !== "object" && !isFunction(target)) {
target = {};
}
類型不一致
其實我們實現的方法有個小 bug ,不信我們寫個 demo:
var obj1 = {
a: 1,
b: {
c: 2
}
}
var obj2 = {
b: {
c: [5],
}
}
var d = extend(true, obj1, obj2)
console.log(d);
我們預期會返回這樣一個對象:
{
a: 1,
b: {
c: [5]
}
}
然而返回了這樣一個對象:
{
a: 1,
b: {
c: {
0: 5
}
}
}
讓我們細細分析為什麽會導致這種情況:
首先我們在函數的開始寫一個 console 函數比如:console.log(1),然後以上面這個 demo 為例,執行一下,我們會發現 1 打印了三次,這就是說 extend 函數執行了三遍,讓我們捋一捋這三遍傳入的參數:
第一遍執行到遞歸調用時:
var src = { c: 2 };
var copy = { c: [5]};
target[name] = extend(true, src, copy);
第二遍執行到遞歸調用時:
var src = 2;
var copy = [5];
target[name] = extend(true, src, copy);
第三遍進行最終的賦值,因為 src 是一個基本類型,我們默認使用一個空對象作為目標值,所以最終的結果就變成了對象的屬性!
為了解決這個問題,我們需要對目標屬性值和待復制對象的屬性值進行判斷:
判斷目標屬性值跟要復制的對象的屬性值類型是否一致:
-
如果待復制對象屬性值類型為數組,目標屬性值類型不為數組的話,目標屬性值就設為 []
-
如果待復制對象屬性值類型為對象,目標屬性值類型不為對象的話,目標屬性值就設為 {}
結合著《JavaScript專題之類型判斷(下)》中的 isPlainObject 函數,我們可以對類型進行更細致的劃分:
var clone, copyIsArray;
...
if (deep && copy && (isPlainObject(copy) ||
(copyIsArray = Array.isArray(copy)))) {
if (copyIsArray) {
copyIsArray = false;
clone = src && Array.isArray(src) ? src : [];
} else {
clone = src && isPlainObject(src) ? src : {};
}
target[name] = extend(deep, clone, copy);
} else if (copy !== undefined) {
target[name] = copy;
}
循環引用
實際上,我們還可能遇到一個循環引用的問題,舉個例子:
var a = {name : b};
var b = {name : a}
var c = extend(a, b);
console.log(c);
我們會得到一個可以無限展開的對象,類似於這樣:
為了避免這個問題,我們需要判斷要復制的對象屬性是否等於 target,如果等於,我們就跳過:
...
src = target[name];
copy = options[name];
if (target === copy) {
continue;
}
...
如果加上這句,結果就會是:
{name: undefined}
最終代碼
// isPlainObject 函數來自於 [JavaScript專題之類型判斷(下) ](https://github.com/mqyqingfeng/Blog/issues/30)
var class2type = {};
var toString = class2type.toString;
var hasOwn = class2type.hasOwnProperty;
function isPlainObject(obj) {
var proto, Ctor;
if (!obj || toString.call(obj) !== "[object Object]") {
return false;
}
proto = Object.getPrototypeOf(obj);
if (!proto) {
return true;
}
Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
return typeof Ctor === "function" && hasOwn.toString.call(Ctor) === hasOwn.toString.call(Object);
}
function extend() {
// 默認不進行深拷貝
var deep = false;
var name, options, src, copy, clone, copyIsArray;
var length = arguments.length;
// 記錄要復制的對象的下標
var i = 1;
// 第一個參數不傳布爾值的情況下,target 默認是第一個參數
var target = arguments[0] || {};
// 如果第一個參數是布爾值,第二個參數是 target
if (typeof target == ‘boolean‘) {
deep = target;
target = arguments[i] || {};
i++;
}
// 如果target不是對象,我們是無法進行復制的,所以設為 {}
if (typeof target !== "object" && !isFunction(target)) {
target = {};
}
// 循環遍歷要復制的對象們
for (; i < length; i++) {
// 獲取當前對象
options = arguments[i];
// 要求不能為空 避免 extend(a,,b) 這種情況
if (options != null) {
for (name in options) {
// 目標屬性值
src = target[name];
// 要復制的對象的屬性值
copy = options[name];
// 解決循環引用
if (target === copy) {
continue;
}
// 要遞歸的對象必須是 plainObject 或者數組
if (deep && copy && (isPlainObject(copy) ||
(copyIsArray = Array.isArray(copy)))) {
// 要復制的對象屬性值類型需要與目標屬性值相同
if (copyIsArray) {
copyIsArray = false;
clone = src && Array.isArray(src) ? src : [];
} else {
clone = src && isPlainObject(src) ? src : {};
}
target[name] = extend(deep, clone, copy);
} else if (copy !== undefined) {
target[name] = copy;
}
}
}
}
return target;
};
思考題
如果覺得看明白了上面的代碼,想想下面兩個 demo 的結果:
var a = extend(true, [4, 5, 6, 7, 8, 9], [1, 2, 3]);
console.log(a) // ???
var obj1 = {
value: {
3: 1
}
}
var obj2 = {
value: [5, 6, 7],
}
var b = extend(true, obj1, obj2) // ???
var c = extend(true, obj2, obj1) // ???
從零實現jQuery的extend