jQuery的extend方法源碼解讀

文章主要分爲三部分,第一部分簡單介紹了extend的語法,第二部分經過實例介紹extend的用途,最後一部分是extend的源碼解讀,同時另附extend的另外一種實現方式。html

1、方法介紹

jQuery 的 API 手冊中,extend 方法掛載在 jQuery 和 jQuery.fn 兩個不一樣的對象上,但在 jQuery 內部代碼實現的是相同的,只是功能各不相同。jquery

官方解釋:ajax

  • jQuery.extend:Merge the contents of two or more objects together into the first object.(把兩個或者多個對象合併到第一個對象當中)編程

  • jQuery.fn.extend:Merge the contents of an object onto the jQuery prototype to provide new jQuery instance methods.(把對象掛載到 jQuery 的 prototype 上以擴展一個新的 jQuery 實例方法 。)json

syntax:segmentfault

  • jQuery.extend([deep,] [target,] object1 [,objectN]);數組

  • jQuery.fn.extend([deep,] [target,] object1 [,objectN])ide

deep: Boolen類型,可選,表示是否進行遞歸合併(深/淺複製),爲true是爲深複製;默認值爲false,淺複製。
target:擴展對象,可選,將接收新的屬性。
objectN:一個對象,包含額外的屬性,擴展到目標對象(擴展對象)。函數

2、extend能實現的功能

將兩個或者更多個對象合併到第一個對象

在這種狀況下,extend方法須要至少傳入兩個對象,語法以下:工具

jQuery.extend(target, object1 [,objectN])
or
jQuery.fn.extend(target, object1 [,objectN])

合併object1,...,objectN對象內容到第一個對象target。
這裏須要注意一下幾點:

1.合併後target對象的內容會改變,若是不但願改變target對象的內容,能夠將第一個對象設置爲{}.
2.這種方法是有返回值的,返回值就是修改後的target對象.
3.合併後的target對象的內容,屬性值永遠是在object1,...,objectN幾個對象中最後一次出現時的屬性值,也就是對於相同名字的屬性,後面對象中的屬性值會覆蓋前面對象的屬性值。

實例

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"}}

覆蓋函數的默認參數

這條用法實際上就是用的「將兩個或者更多個對象合併到第一個對象」,之因此把提出來另起一個標題,是由於這是一種很常見的編程技巧。

實例:

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"}

函數getOpt含有默認參數(對象)_default,若傳入函數的參數option(對象)中含有某個屬性的值,則使用傳入值,不然使用默認值。

深淺拷貝

所謂的深淺拷貝,就是C語言中的拷貝地址與數據

淺拷貝

淺複製對象A時,對象B將複製A的全部字段,若是字段是內存地址,B將複製地址,若果字段是基元類型,B將複製其值。
淺複製的缺點是若是你改變了對象B所指向的內存地址,你同時也改變了對象A指向這個地址的字段

function copy(target,cloneObj){
    for(var i in cloneObj){
        target[i]  = cloneObj[i];
    }
    return target;
}
var a = {
    a:{ c:"c" },
    b:"b"
}
var t = {};
copy(t,a);
t.a.c ="e";
console.log(a.a.c);//e

深拷貝

這種方式會徹底複製全部數據,優勢是B與A不會相互依賴(A,B徹底脫離關聯), 缺點是複製的速度慢,代價大。

一種是實現深度拷貝的方案:

function type(obj){
    return Object.prototype.toString.call(obj).slice(8,-1);
}
function deepCopy(target,cloneObj){
    var copy;
    for(var i in cloneObj){
        copy = cloneObj[i];
        if(target === copy){
            continue;
        }
        if(type(copy) === "Array"){
            target[i] = arguments.callee(target[i] || [],copy);
        }else if(type(copy) === "Object"){
            target[i] = arguments.callee(target[i] || {},copy);
        }else{
            target[i] = copy;
        }
    }
    return target;
}

var a = {
    a:{ c:"c" },
    b:"b"
}
var t = deepCopy({},a);
t.a.c ="e";
console.log(a.a.c);//c

注意關於arguments,caller,callee不懂的請移步這裏JavaScript 之arguments、caller 和 callee 介紹 .
能夠看到a沒有被修改,可是要更深層次的遍歷,確定很耗費性能的。用for-in把全部可枚舉的包括原型鏈上的一塊兒遍歷了。

用法

在用extend方法進行對象合併時,能夠指定第一個參數爲boolean類型,來決定是深拷貝(true)仍是淺拷貝(false),語法以下:

jQuery.extend(deep, target, object1 [,objectN])
or
jQuery.fn.extend(deep, target, object1 [,objectN])

實例

var obj1 = {
    name: "John",
    location: {
        city: "Boston",
        county: "USA"
    }
}

var obj2 = {
    last: "Resig",
    location: {
        state: "MA",
        county: "China"
    }
}

$.extend(false, {}, obj1, obj2); // { name: "John", last: "Resig", location: { state: "MA", county: "China" }}

$.extend(true, {}, obj1, obj2); // { name: "John", last: "Resig", location: { city: "Boston", state: "MA", county: "China" }}

因而可知,執行 深度複製 會遞歸遍歷每一個對象中含有複雜對象(如:數組、函數、json對象等)的屬性值進行復制,並且 淺度複製 便不會這麼作。

jQuery插件開發

jQuery插件開發分爲兩種:1 類級別、2 對象級別

  • 類級別(類方法):是直接可使用類引用,不須要實例化就可使用的方法。通常在項目中 類方法 都是被設置爲工具類使用;

  • 對象級別(實例方法)必須先建立實例,而後才能經過實例調用該 實例方法

jQuery能夠看作是這個封裝得很是好的類,而咱們可使用jQuery選擇器來建立 jQuery 的實例。好比:使 id 選擇器$('#btn')來建立一個實例。

類級別 $.extend(src)

類級別你能夠理解爲拓展jQuery類,最明顯的例子是$.ajax(...),至關於靜態方法,開發擴展其方法時使用$.extend方法
實例1

$.extend({
    add:function(a,b){return a+b;} 
    minus:function(a,b){return a-b;}
}); 

調用方式

var i = $.add(3,2);
var j = $.minus(3,2);

對象級別 $.fn.extend(src)

對象級別則能夠理解爲基於對象的拓展,如$("#table").set(...); 這裏這個set呢,就是基於對象的拓展了。開發擴展其方法時使用$.fn.extend方法,

$.fn.extend({

    check:function(){
        return this.each({
            this.checked=true;
        });
    },
    uncheck:function(){
        return this.each({
            this.checked=false;
        });
    }
}); 

調用方式

$('input[type=checkbox]').check();
$('input[type=checkbox]').uncheck();

相似於命名空間的擴展

$.xy = {
    add:function(a,b){
        return a+b;
    } ,
    minus:function(a,b){
        return a-b;
    },
    voidMethod:function(){
        alert("void");
    }
};
調用方式
var i = $.xy.add(3,2);
var m = $.xy.minus(3,2);
$.xy.voidMethod();

用法

若是隻有一個參數對象提供給$.extend(),這意味着目標參數被省略。在這種狀況下,調用extend方法的對象被默認爲目標對象,參數對象中的內容將合併到目標對象中去。語法以下:

jQuery.extend(object)
or
jQuery.fn.extend(object)
1.$.extend(src)

該方法就是將src合併到jquery的全局對象中去,如:

$.extend({
  hello:function(){alert('hello');}
  });

就是將hello方法合併到jquery的全局對象中。

2.$.fn.extend(src)

該方法將src合併到jquery的實例對象中去,如:

$.fn.extend({
  hello:function(){alert('hello');}
 });

就是將hello方法合併到jquery的實例對象中。

3、源碼解讀

// 爲與源碼的下標對應上,咱們把第一個參數稱爲`第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;
};

若參數中字段的值就是目標參數,中止賦值

在源碼中進行了一下這樣的判斷:

// 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中直接不進行操做,跳過這個字段,進行下一個字段的操做。

深度拷貝時進行遞歸處理

變量值爲簡單類型(基元類型,如number, string, boolean)進行賦值時是不會影響上一個變量的值的,所以,若是當前字段的值爲Object或Array類型,須要對其進行拆分,直到字段的值爲簡單類型(如number, string, boolean)時才進行賦值操做。

$.extend()與$.fn.extend()

jQuery.extend = jQuery.fn.extend = function(){}

也就是說$.extend()$.fn.extend()共用的是同一個函數體,全部的操做都是同樣的,只不過兩個extend使用的對象不一樣罷了:$.extend()是在jQuery($)上進行操做的;而$.fn.extend()是在jQuery對象上進行操做的,如$('div').extend().

4、另外一種實現方式

版本1:

void function(global){
    var extend,
        _extend,
        _isObject;

    _isObject = function(o){
        return Object.prototype.toString.call(o) === '[object Object]';
    }

    _extend = function self(destination, source){
        for (var property in source) {
            if (source.hasOwnProperty(property)) {

                // 若sourc[property]是對象,則遞歸
                if (_isObject(source[property])) {

                    // 若destination沒有property,賦值空對象
                    if (!destination.hasOwnProperty(property)) {
                        destination[property] = {};
                    };

                    // 對destination[property]不是對象,賦值空對象
                    if (!_isObject(destination[property])) {
                        destination[property] = {};
                    };

                    // 遞歸
                    self(destination[property], source[property]);
                } else {
                    destination[property] = source[property];
                };
            }
        }
    }

    extend = function(){
        var arr = arguments,
            result = {},
            i;

        if (!arr.length) return {};

        for (i = 0; i < arr.length; i++) {
            if (_isObject(arr[i])) {
                _extend(result, arr[i])
            };
        }

        arr[0] = result;
        return result;
    }

    global.extend = extend;
}(window)

版本1存在的問題:咱們這裏是按照參數順序從左到右依次執行的,可是其實如果最後一個參數有的屬性,前面的參數上的該屬性都不須要再擴展了。其實前面的全部參數都是將本身身上有的屬性而最後一個參數沒有的屬性補到最後一個參數上。既如此,是否是從參數列表的右側開始擴展更好一些。

版本2

void function(global){
    var extend,
        _extend,
        _isObject;

    _isObject = function(o){
        return Object.prototype.toString.call(o) === '[object Object]';
    }

    _extend = function self(destination, source) {
        var property;
        for (property in destination) {
            if (destination.hasOwnProperty(property)) {

                // 若destination[property]和sourc[property]都是對象,則遞歸
                if (_isObject(destination[property]) && _isObject(source[property])) {
                    self(destination[property], source[property]);
                };

                // 若sourc[property]已存在,則跳過
                if (source.hasOwnProperty(property)) {
                    continue;
                } else {
                    source[property] = destination[property];
                }
            }
        }
    }

    extend = function(){
        var arr = arguments,
            result = {},
            i;

        if (!arr.length) return {};

        for (i = arr.length - 1; i >= 0; i--) {
            if (_isObject(arr[i])) {
                _extend(arr[i], result);
            };
        }

        arr[0] = result;
        return result;
    }

    global.extend = extend;
}(window)

5、參考

  1. 舉例分析jQuery.extend()方法

  2. jquery中extend的實現

  3. 深刻剖析 jQuery 的 extend 方法

  4. jQuery.extend 函數詳解

  5. How to Create a Basic Plugin

  6. jQuery Plugin開發

  7. Jquery中extend該怎麼寫,有幾種寫法,分別用在什麼場景?

  8. JavaScript 實現 extend

相關文章
相關標籤/搜索