Javascript筆記:(實踐篇)從jQuery外掛技術說起-分析extend方法的原始碼(發現extend方法裡有bug)(下篇)...

weixin_34162629發表於2012-05-06

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的原始碼。

 

相關文章