通常我們使用jquery的extend時,大都是為了實現預設欄位的覆蓋,即若傳入某個欄位的值,則使用傳入值,否則使用預設值。如下面的程式碼:
function getOpt(option){
var _default = {
name : `wenzi`,
age : `25`,
sex : `male`
}
$.extend(_default, option);
return _default;
}
getOpt(); // {name: "wenzi", age: "25", sex: "male"}
getOpt({name:`bing`}); // {name: "bing", age: "25", sex: "male"}
getOpt({name:`bing`, age:36, sex:`female`}); // {name: "bing", age: 36, sex: "female"}
那現在我們就得需要知道這個extend具體是怎麼實現的了,除了實現上面的功能,還有其他作用麼?那肯定是有的啦,否則我也不會問那句話了((⊙﹏⊙)b)。我們先來看看extend主要有哪些功能,然後再看實現這些功能的原理。
1. extend能實現的功能
其實從extend的含義裡,我們就能知道extend是做什麼的了。extend翻譯成漢語後就是:延伸、擴充套件、推廣。
1.1 將兩個或更多物件的內容合併到第一個物件
我們來看看$.extend()
提供的引數:jQuery.extend( target [, object1 ] [, objectN ] )
,extend方法需要至少傳入一個引數,第一個必需,後面的都是可選引數。若傳給extend是兩個或兩個以上的引數都是物件型別,那麼就會把後面所有物件的內容合併給target(第一個物件)上。
我們再來看看上面的例子:
function getOpt(option){
var _default = {
name : `wenzi`,
age : `25`,
sex : `male`
}
`$.extend(_default, option);`
return _default;
}
$.extend()
中接收了兩個引數_default和option,那麼extend方法執行時,就會把option物件上欄位的值全給了_default。於是_default上設定的預設值就會被option上的值覆蓋。當然,若option上沒有這個欄位,就不會覆蓋_default上欄位的值。
上面函式中的extend,只是傳入了兩個引數,那傳的引數再更多一些呢:
function getOpt(target, obj1, obj2, obj3){
$.extend(target, obj1, obj2, obj3);
return target;
}
var _default = {
name : `wenzi`,
age : `25`,
sex : `male`
}
var obj1 = {
name : `obj1`
}
var obj2 = {
name : `obj2`,
age : `36`
}
var obj3 = {
age : `67`,
sex : {`error`:`sorry, I dont`t kown`}
}
getOpt(_default, obj1, obj2, obj3); // {name: "obj2", age: "67", sex: {error: "sorry, I dont`t kown"}}
這裡我們傳入了4個引數,然後getOpt()返回第一個引數的值。從執行的得到結果我們可以看到,屬性值永遠是最後一個屬性的值。
還有很重要的一點,$.extend()
其實是有返回值的,返回的就是修改後的第一個引數的值。如我們可以把上面的函式修改成這樣:
function getOpt(target, obj1, obj2, obj3){
var result = $.extend(target, obj1, obj2, obj3);
return result; // // result即修改後的target值
}
若我們傳入的引數不想被修改,我們可以用一個空物件來作為第一個引數,然後獲取$.extend()
的返回值:
function getOpt(target, obj1, obj2, obj3){
var result = $.extend({}, target, obj1, obj2, obj3);
return result; // // result即為{}修改後的值
}
1.2 為jQuery擴充套件方法或屬性
剛才我們在1.1中講的$.extend()
的例子都是傳了兩個或兩個以上的引數,但其實只有一個引數是必須的。若只傳一個引數會怎樣呢。
如果只有一個引數提供給
$.extend()
,這意味著目標引數被省略。在這種情況下,jQuery物件本身被預設為目標物件。這樣,我們可以在jQuery的名稱空間下新增新的功能。這對於外掛開發者希望向 jQuery 中新增新函式時是很有用的。
$.extend({
_name : `wenzi`,
_getName : function(){
return this._name;
}
})
$._name; // wenzi
$._getName(); // wenzi
這樣我們就為jQuery擴充套件了_name
屬性和_getName
方法。
1.3 深度拷貝和淺度拷貝
針對什麼是深度拷貝,什麼是淺度拷貝,我們先來看一個簡單的例子。
var obj = {name:`wenzi`, sex:`male`};
var obj1 = obj; // 賦值
obj1.name = `bing`;
console.log(obj.name); // bing
我們修改了obj1中的name值,結果obj中的值也跟著發生了變化,這是為什麼呢。其實這就是淺度拷貝
:這僅僅是將obj物件的引用地址簡單的複製了一份給予變數 obj1,而並不是將真正的物件克隆了一份,因此obj和obj1指向的都是同一個地址。當修改obj1的屬性或給obj1新增新屬性時,obj都會受到影響。
可是如果變數的值不是物件和陣列,修改後面的變數是不會影響到前面的變數:
var s = `hello`;
var t = s;
t = `world`;
console.log(s); // hello
那麼深度拷貝就不是拷貝引用地址,而是實實在在的複製一份新物件給新的變數。 在上面使用$.extend()
中,都是使用的淺度拷貝,因此若後面的引數值是object型別或array型別,修改_default(target)的值,就會影響後面引數的值。
如我們使用getOpt(_default, obj1, obj2, obj3);
得到的_default值是{name: “obj2”, age: “67”, sex: {error: “sorry, I dont`t kown”}},可是若:
_default.sex.error = `hello world`;
那麼obj3.sex.error也會跟著修改,因為obj3.sex是一個object型別。
不過$.extend()
也提供了深度拷貝的方法:jQuery.extend( [deep ], target, object1 [, objectN ] )。若第一個引數是boolean型別,且值是true,那麼就會把第二個引數作為目標引數進行合併。
var obj = {name:`wenzi`, score:80};
var obj1 = {score:{english:80, math:90}}
$.extend(true, obj, obj1);
obj.score.english = 10;
console.log(obj.score.english); // 10
console.log(obj1.score.english); // 80
執行後我們發現,無論怎麼修改obj.score裡的值,都不會影響到obj1.score了。
2. jQuery中extend實現原理
其實不看原始碼,對extend大致的過程應該也是瞭解的:對後一個引數進行迴圈,然後把後面引數上所有的欄位都給了第一個欄位,若第一個引數裡有相同的欄位,則進行覆蓋操作,否則就新增一個新的欄位。
下面是jQuery中關於extend的原始碼,我就在原始碼上進行註釋講解了,隨後再在後面進行總結:
// 為與原始碼的下標對應上,我們把第一個引數稱為`第0個引數`,依次類推
jQuery.extend = jQuery.fn.extend = function() {
var options, name, src, copy, copyIsArray, clone,
target = arguments[0] || {}, // 預設第0個引數為目標引數
i = 1, // i表示從第幾個引數凱斯想目標引數進行合併,預設從第1個引數開始向第0個引數進行合併
length = arguments.length,
deep = false; // 預設為淺度拷貝
// 判斷第0個引數的型別,若第0個引數是boolean型別,則獲取其為true還是false
// 同時將第1個引數作為目標引數,i從當前目標引數的下一個
// Handle a deep copy situation
if ( typeof target === "boolean" ) {
deep = target;
// Skip the boolean and the target
target = arguments[ i ] || {};
i++;
}
// 判斷目標引數的型別,若目標引數既不是object型別,也不是function型別,則為目標引數重新賦值
// Handle case when target is a string or something (possible in deep copy)
if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
target = {};
}
// 若目標引數後面沒有引數了,如$.extend({_name:`wenzi`}), $.extend(true, {_name:`wenzi`})
// 則目標引數即為jQuery本身,而target表示的引數不再為目標引數
// Extend jQuery itself if only one argument is passed
if ( i === length ) {
target = this;
i--;
}
// 從第i個引數開始
for ( ; i < length; i++ ) {
// 獲取第i個引數,且該引數不為null,
// 比如$.extend(target, {}, null);中的第2個引數null是不參與合併的
// Only deal with non-null/undefined values
if ( (options = arguments[ i ]) != null ) {
// 使用for~in獲取該引數中所有的欄位
// Extend the base object
for ( name in options ) {
src = target[ name ]; // 目標引數中name欄位的值
copy = options[ name ]; // 當前引數中name欄位的值
// 若引數中欄位的值就是目標引數,停止賦值,進行下一個欄位的賦值
// 這是為了防止無限的迴圈巢狀,我們把這個稱為,在下面進行比較詳細的講解
// Prevent never-ending loop
if ( target === copy ) {
continue;
}
// 若deep為true,且當前引數中name欄位的值存在且為object型別或Array型別,則進行深度賦值
// Recurse if we`re merging plain objects or arrays
if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
// 若當前引數中name欄位的值為Array型別
// 判斷目標引數中name欄位的值是否存在,若存在則使用原來的,否則進行初始化
if ( copyIsArray ) {
copyIsArray = false;
clone = src && jQuery.isArray(src) ? src : [];
} else {
// 若原物件存在,則直接進行使用,而不是建立
clone = src && jQuery.isPlainObject(src) ? src : {};
}
// 遞迴處理,此處為2.2
// Never move original objects, clone them
target[ name ] = jQuery.extend( deep, clone, copy );
// deep為false,則表示淺度拷貝,直接進行賦值
// 若copy是簡單的型別且存在值,則直接進行賦值
// Don`t bring in undefined values
} else if ( copy !== undefined ) {
// 若原物件存在name屬性,則直接覆蓋掉;若不存在,則建立新的屬性
target[ name ] = copy;
}
}
}
}
// 返回修改後的目標引數
// Return the modified object
return target;
};
原始碼分析完了,下面我們來講解下原始碼中存在的幾個難點和重點。
2.1 若引數中欄位的值就是目標引數,停止賦值
在原始碼中進行了一下這樣的判斷:
// Prevent never-ending loop
if ( target === copy ) {
continue;
}
為什麼要有這樣的判斷,我們來看一個簡單的例子,如果沒有這個判斷會怎麼樣:
var _default = {name : `wenzi`};
var obj = {name : _default}
$.extend(_default, obj);
console.log(_default);
輸出的_default是什麼呢:
_default = {name : _default};
_default是object型別,裡面有個欄位name,值是_default,而_default是object型別,裡面有個欄位name,值是_default……,無限的迴圈下去。於是jQuery中直接不進行操作,跳過這個欄位,進行下一個欄位的操作。
2.2 深度拷貝時進行遞迴處理
我們在前面稍微的講解了一下,變數值為簡單型別(如number, string, boolean)進行賦值時是不會影響上一個變數的值的,因此,如果當前欄位的值為Object
或Array
型別,需要對其進行拆分,直到欄位的值為簡單型別(如number, string, boolean)時才進行賦值操作。
3. $.extend()與$.fn.extend()
上面講解的全都是$.extend(),根本就沒講$.fn.extend()。可是,你有沒有發現一個細節,在這段程式碼的第一行是怎麼寫的:
jQuery.extend = jQuery.fn.extend = function(){}
也就是說$.extend()與$.fn.extend()共用的是同一個函式體,所有的操作都是一樣的,只不過兩個extend使用的物件不同罷了:$.extend()
是在jQuery($)上進行操作的;而$.fn.extend()
是在jQuery物件上進行操作的,如$(`div`).extend().
4. 總結
這就是jQuery中extend的實現,以後若我們需要用到上面的功能時,除了使用$.extend(),我們也可以在不引入jQuery框架的情況下,自己寫一個簡單的extend()來實現上面的功能。
本文正式地址:http://www.xiabingbao.com/jquery/2015/05/30/jquery-extend