jQuery的extend方法原始碼解讀

jack2wang發表於2016-07-07

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

一、方法介紹

jQuery 的 API 手冊中,extend 方法掛載在 jQuery 和 jQuery.fn 兩個不同的物件上,但在 jQuery 內部程式碼實現的是相同的,只是功能各不相同。

官方解釋:

  • 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 例項方法 。)

syntax:

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

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

deep: Boolen型別,可選,表示是否進行遞迴合併(深/淺複製),為true是為深複製;預設值為false,淺複製。
target:擴充套件物件,可選,將接收新的屬性。
objectN:一個物件,包含額外的屬性,擴充套件到目標物件(擴充套件物件)。

二、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的例項物件中。

三、原始碼解讀

// 為與原始碼的下標對應上,我們把第一個引數稱為`第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().

四、另一種實現方式

版本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)

五、參考

  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

相關文章