1.1 分析$.extend原始碼
在分析原始碼之前,我還要加一段s測試程式碼,程式碼如下:
<script type="text/javascript"> $(document).ready(function(){ console.log('==================測試06 start'); var targetobj = {'id':'NO1111','name':'xiajun','age':23,'sex':'man','comment':{'test01':'t01','test02':'t02','test03':'t03'}}, srcobj = {'id':'NO1122','name':'sharp','comment':{'test01':'tt001','test02':'tt002','test04':'t04'}}; var resobj = $.xjcopy(targetobj,srcobj); console.log(resobj);//Object { id="NO1122", name="sharp", age=23, sex="man",comment=Object { test01="tt001", test02="tt002", test04="t04"}} console.log(targetobj);//Object { id="NO1122", name="sharp", age=23, sex="man",comment=Object { test01="tt001", test02="tt002", test04="t04"}} targetobj = {'id':'NO1111','name':'xiajun','age':23,'sex':'man','comment':{'test01':'t01','test02':'t02','test03':'t03'}}, srcobj = {'id':'NO1122','name':'sharp','comment':{'test01':'tt001','test02':'tt002','test04':'t04'}}; resobj = $.xjcopy(false,targetobj,srcobj); console.log(resobj);//Object { id="NO1122", name="sharp", age=23, sex="man",comment=Object { test01="tt001", test02="tt002", test04="t04"}} console.log(targetobj);//Object { id="NO1111", name="xiajun", age=23, sex="man",comment=Object { test01="t01", test02="t02", test03="t03"}} targetobj = {'id':'NO1111','name':'xiajun','age':23,'sex':'man','comment':{'test01':'t01','test02':'t02','test03':'t03'}}, srcobj = {'id':'NO1122','name':'sharp','comment':{'test01':'tt001','test02':'tt002','test04':'t04'}}; resobj = $.xjcopy(true,targetobj,srcobj); console.log(resobj);//Object { id="NO1122", name="sharp", age=23, sex="man",comment=Object { test01="tt001", test02="tt002", test03="t03",test04="t04"}} console.log(targetobj);//Object { id="NO1122", name="sharp", age=23, sex="man",comment=Object { test01="tt001", test02="tt002", test03="t03",test04="t04"}} console.log('==================測試06 end'); }); </script>
我前面寫的測試例項裡沒有大物件裡包含小物件的情況,而深淺拷貝關鍵場景就是物件包含物件的特殊場景,因此這裡把這種場景補上。從列印出來的結果我們看到當我們不設定deep屬性時候,非comment屬性的值是targetobj和srcobj合併的結果,而comment的返回值是srcobj的comment的值,同時extend方法的返回值和target的值是相同;當我們設定了deep屬性的值,如果deep值為false時候,我們發現extend的返回值和不設定deep屬性時候值是一樣的,但是targetobj的值是不會改變的,有個朋友問我,當deep屬性設定為false,我們看到targetobj值沒變,但是如果srcobj的屬性個數超過了targetobj的個數,那麼srcobj的多餘屬性會不會合併到targetobj呢?為了這個問題我再寫了一個測試,程式碼如下:
targetobj = {'id':'NO1111','name':'xiajun','age':23,'comment':{'test01':'t01','test02':'t02','test03':'t03'}}, srcobj = {'id':'NO1122','name':'sharp','sex':'man','comment':{'test01':'tt001','test02':'tt002','test04':'t04'}}; resobj = $.xjcopy(false,targetobj,srcobj); console.log(resobj);//Object { id="NO1122", name="sharp", age=23, sex="man",comment=Object { test01="tt001", test02="tt002", test04="t04"}} console.log(targetobj);//Object { id="NO1111", name="xiajun", age=23, comment=Object { test01="t01", test02="t02", test03="t03"}}
我們發現targetobj的值的確沒變;如果我們把deep的值設定為true,那麼從結果我們看到targetobj和extend方法的返回值都是被合併後的結果。
呵呵,對於extend方法的使用現在比較清晰了吧,我們看測試結果會發現:
jQuery裡的extend方法存在著bug,如果我們不設定一個引數deep的值最終結果當然不是deep為true所代表的深拷貝,但是它和deep屬性設為false時候結果也有不同,目標物件target一個被改變一個沒有任何變化,所以不設定deep屬性的值也不能說是deep設為false的預設操作,這個應該算是extend方法的bug吧
下面我們讀讀extend方法的原始碼,看看到底是什麼樣的原因產生了這樣的結果。
原始碼的第一部分是為extend方法內部設定一些需要使用的區域性變數,程式碼如下:
// options是用來儲存拷貝物件的源物件的臨時變數,name是拷貝物件的屬性值 // src 用來儲存拷貝物件目標的值,copy儲存被拷貝物件的目標值比如我們示例程式碼 // 裡的 src = targetobj['id'],copy = targetobj['id'] // copyIsArray是個布林值,用來儲存物件是不是陣列的標記 // clone是作為合併的的臨時物件(這個大家看深拷貝的程式碼慢慢體會了) // target這個是我們的extend引數的target // i是extend引數objectN在arguments裡的索引值 // length是指引數個數,deep深淺拷貝的標記,大家可以看到預設下deep是false var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {}, i = 1, length = arguments.length, deep = false;
講解寫在註釋裡的這裡就不在累述了,我們接著讀下面的程式碼:
// 如果第一個引數是布林值,那麼這是使用者在設定是否要進行深淺拷貝 if ( typeof target === "boolean" ) { deep = target;//設定deep的值,這個好理解 target = arguments[1] || {};//如果設定deep屬性,那麼target要重新賦值 // 如果第一個引數設定的深淺拷貝標記,那麼i設為2,表明arguments的objectN引數是從索引為2的值開始 i = 2; }
註釋比較清晰,這裡也不囉嗦了。
不過這段程式碼是有問題,是有bug的。這段程式碼也是我們不設定第一個引數deep值和deep值設為false最後結果不一致所在,其實程式碼作者的原意是deep不被設定時候的結果和deep設定為false是一樣的。但是如果引數為false,typeof判斷型別不是boolean而是object
大家看下面的測試程式碼就明白其中道理了:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>無標題文件</title> </head> <body> </body> </html> <script type="text/javascript"> window.onload = function(){ //alert(typeof false);//boolean //alert(typeof true);//boolean typeftn(false,{},{}); typeftn(true,{},{}); } function typeftn(deep,target,src){ var bobj = arguments[0] || {}; //console.log(typeof bobj);//如果是false,結果是object,如果是true結果就是boolean alert(typeof bobj); } </script>
先擱置一下這個bug,我們接著讀extend原始碼:
// 如果傳入的不是物件或者是函式,可能為字串,那麼把target = {}置為空物件 // extend最終返回值是target,我們看到target在方法裡都是被設定為物件,所以不管我們傳什麼樣的 // 的引數到extend方法裡,最終結果都是object型別,這也說明如果objectN引數是物件,extend同樣會做合併操作 // 如果objectN非object型別那麼下面深淺拷貝操作也就沒意義了 if ( typeof target !== "object" && !$.isFunction(target) ) { target = {}; }
下面的程式碼就是為什麼extend可以編寫外掛的原因了,程式碼如下:
// 當引數只有一個時候,target被設定為this,這裡就是關鍵所在,這裡的解釋我要寫在文件裡 if ( length === i ) { target = this; --i; }
當我們只傳一個引數並且這個引數是object型別,那麼target把設定為this,所以當我們按jQuery.extend方式呼叫extend方法,那麼this會指向jQuery物件,程式碼後面寫—i,那麼表明當我們只傳一個引數時候,這個引數不是target引數而是變成了objectN引數,成為了被拷貝物件,最終引數的內容會被拷貝到jQuery物件內部,最終成為jQuery的全域性物件的一個屬性。
接下來的程式碼就是做深淺拷貝的程式碼,這個程式碼我在前面已經寫過,這裡也不多講了,程式碼如下:
// 下面的程式碼就是javascript裡做深淺拷貝的程式碼,這個和我們前面自己寫的深淺拷貝的程式碼類似 for ( ; i < length; i++ ) { // 只操作物件值非null/undefined的資料 if ( (options = arguments[i]) != null ) { for ( name in options ) { src = target[ name ]; copy = options[ name ]; // 避免死迴圈,這個和我寫的深拷貝的程式碼類似 if ( target === copy ) { continue; } // 通過遞迴的方式我們把物件和陣列型別的資料合併起來 if ( deep && copy && ( $.isPlainObject(copy) || (copyIsArray = $.isArray(copy)) ) ) { if ( copyIsArray ) { copyIsArray = false; clone = src && $.isArray(src) ? src : []; } else { clone = src && $.isPlainObject(src) ? src : {}; } // 不去改變原始物件,只是對原始物件做拷貝操作 target[ name ] = $.xjcopy( deep, clone, copy ); } else if ( copy !== undefined ) { target[ name ] = copy; } } } } // 返回結果 return target;
下面我將我寫的和extend方法一模一樣的的xjcopy方法改寫下,修正extend方法裡的bug,程式碼如下:
;(function($){ $.xjcopy = $.fn.xjcopy = function(){ // options是用來儲存拷貝物件的源物件的臨時變數,name是拷貝物件的屬性值 // src 用來儲存拷貝物件目標的值,copy儲存被拷貝物件的目標值比如我們示例程式碼 // 裡的 src = targetobj['id'],copy = targetobj['id'] // copyIsArray是個布林值,用來儲存物件是不是陣列的標記 // clone是作為合併的的臨時物件(這個大家看深拷貝的程式碼慢慢體會了) // target這個是我們的extend引數的target // i是extend引數objectN在arguments裡的索引值 // length是指引數個數,deep深淺拷貝的標記,大家可以看到預設下deep是false var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {}, i = 1, length = arguments.length, deep = false; // 如果第一個引數是布林值,那麼這是使用者在設定是否要進行深淺拷貝 if (length >2){ if (deep === false || deep === true){ deep = target;//設定deep的值,這個好理解 target = arguments[1] || {};//如果設定deep屬性,那麼target要重新賦值 // 如果第一個引數設定的深淺拷貝標記,那麼i設為2,表明arguments的objectN引數是從索引為2的值開始 i = 2; } } /*if ( typeof target === "boolean") { deep = target;//設定deep的值,這個好理解 target = arguments[1] || {};//如果設定deep屬性,那麼target要重新賦值 // 如果第一個引數設定的深淺拷貝標記,那麼i設為2,表明arguments的objectN引數是從索引為2的值開始 i = 2; }*/ // 如果傳入的不是物件或者是函式,可能為字串,那麼把target = {}置為空物件 // extend最終返回值是target,我們看到target在方法裡都是被設定為物件,所以不管我們傳什麼樣的 // 的引數到extend方法裡,最終結果都是object型別,這也說明如果objectN引數是物件,extend同樣會做合併操作 // 如果objectN非object型別那麼下面深淺拷貝操作也就沒意義了 if ( typeof target !== "object" && !$.isFunction(target) ) { target = {}; } // 如果傳入的引數只有一個,跳過下面的步驟 // 當引數只有一個時候,target被設定為this,這裡就是關鍵所在,這裡的解釋我要寫在文件裡 if ( length === i ) { target = this; --i; } // 下面的程式碼就是javascript裡做深淺拷貝的程式碼,這個和我們前面自己寫的深淺拷貝的程式碼類似 for ( ; i < length; i++ ) { // 只操作物件值非null/undefined的資料 if ( (options = arguments[i]) != null ) { for ( name in options ) { src = target[ name ]; copy = options[ name ]; // 避免死迴圈,這個和我寫的深拷貝的程式碼類似 if ( target === copy ) { continue; } // 通過遞迴的方式我們把物件和陣列型別的資料合併起來 if ( deep && copy && ( $.isPlainObject(copy) || (copyIsArray = $.isArray(copy)) ) ) { if ( copyIsArray ) { copyIsArray = false; clone = src && $.isArray(src) ? src : []; } else { clone = src && $.isPlainObject(src) ? src : {}; } // 不去改變原始物件,只是對原始物件做拷貝操作 target[ name ] = $.xjcopy( deep, clone, copy ); } else if ( copy !== undefined ) { target[ name ] = copy; } } } } // 返回結果 return target; }; })(jQuery)
關於extend的內容我這裡講完了。這裡我要說明下,jQuery裡extend方法並不代表著jQuery外掛技術,只能說extend是實現jQuery外掛技術的一種手段。jQuery的外掛技術還有很多內容,其中就包括不使用extend實現外掛的方式,關於外掛的詳細內容我會在以後的部落格裡寫道。
最後我還想說說,對jQuery外掛技術的深入理解可能是理解jQuery原始碼的一把很重要的鑰匙,等我寫完了對jQuery外掛技術的介紹,我就會繼續我臨摹jQuery的系列,好好分析下jQuery的原始碼。