首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 更多程式語言 >> 更多程式設計知識

jQuery.extend() 與 jQuery.fn.extend() 深度解析:原理、差異與插件開發實戰

     編輯:更多程式設計知識

jQuery 的擴展機制是其生態系統繁榮的基石。jQuery.extend() 與 jQuery.fn.extend() 雖然核心代碼共用,但作用對象與場景截然不同。本文從源碼層面深度解析兩者的實現原理、參數處理邏輯、深度合併機制,並結合插件開發實戰,幫助你徹底掌握 jQuery 擴展體系。

核心差異一覽

方法掛載目標用途典型場景
jQuery.extend()jQuery 建構函數 (靜態方法)擴展工具函數、類別方法、全域選擇器$.ajax, $.each, $.fn.xxx 定義
jQuery.fn.extend()jQuery.prototype (實例方法)擴展 jQuery 物件實例方法 (插件)$('#id').plugin() 形式調用

關鍵關係:jQuery.fn === jQuery.prototype,且 jQuery.fn.extend = jQuery.extend(指向同一函數)。差別在於 this 指向:前者 this === jQuery,後者 this === jQuery.prototype。

源碼核心邏輯深度剖析 (jQuery 1.7.1 為例)

jQuery.extend = jQuery.fn.extend = function() {
    var options, name, src, copy, copyIsArray, clone,
        target = arguments[0] || {},
        i = 1,
        length = arguments.length,
        deep = false;

    // 1. 處理 deep 參數:第一個參數為 boolean 時
    if (typeof target === "boolean") {
        deep = target;
        target = arguments[1] || {};
        i = 2;
    }

    // 2. 確保 target 是物件或函數
    if (typeof target !== "object" && !jQuery.isFunction(target)) {
        target = {};
    }

    // 3. 單參數模式:$.extend(obj) -> 擴展 jQuery 自身
    if (length === i) {
        target = this;
        --i;
    }

    // 4. 核心合併迴圈
    for (; i < length; i++) {
        if ((options = arguments[i]) != null) {
            for (name in options) {
                src = target[name];
                copy = options[name];

                // 防止循環引用
                if (target === copy) continue;

                // 5. 深度合併遞歸
                if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
                    if (copyIsArray) {
                        copyIsArray = false;
                        clone = src && jQuery.isArray(src) ? src : [];
                    } else {
                        clone = src && jQuery.isPlainObject(src) ? src : {};
                    }
                    // 遞歸合併,結果賦回目標
                    target[name] = jQuery.extend(deep, clone, copy);
                }
                // 6. 淺層合併:直接覆蓋 (undefined 不覆蓋)
                else if (copy !== undefined) {
                    target[name] = copy;
                }
            }
        }
    }
    return target;
};

五大核心機制詳解

1. 參數靈活處理:arguments 與 deep 旗標

支援多種呼叫形式:

// 單參數:擴展 jQuery 自身 (target = this)
$.extend({ myUtil: function() {} });

// 兩參數:淺層合併 target <- src
$.extend(target, src);

// 三參數:深度合併
$.extend(true, target, src1, src2);

// 目標為空物件,不修改原物件
var result = $.extend({}, obj1, obj2);

2. 目標物件修正:length === i 判斷

當只傳入來源物件時 (如 $.extend({a:1})),length=1, i=1,滿足條件,將 target=this (即 jQuery 或 jQuery.fn),實現「擴展自身」語義。

3. 循環引用防護:target === copy

防止 o = {}; o.self = o; $.extend(true, o, {other: o}) 導致無限遞歸。但此檢查較粗略,可能誤判合法共享引用 (見文中 obj2={a:obj1} 例子)。

4. 深度合併的型別保持

var obj1 = { arr: [1,2], obj: {x:1} };
var obj2 = { arr: [3], obj: {y:2} };

$.extend(true, obj1, obj2);
// 結果: { arr: [3], obj: {x:1, y:2} }
// 注意:陣列被整體替換,非合併;物件屬性合併

陣列視為原子值替換,物件才遞歸合併。這是設計決策,避免陣列索引合併產生不可預期結果。

5. 非物件來源的特殊處理

$.extend({a:1}, 'hello');
// 結果: {0:'h', 1:'e', 2:'l', 3:'l', 4:'o', a:1}
// 字串可枚舉索引,被視為類陣列物件合併

深度合併時,jQuery.isPlainObject('hello') 為 false,故不遞歸,直接淺層合併字串索引屬性。

實戰範例:從基礎到插件開發

基礎合併操作

// 合併兩個普通物件 (淺層)
var a = {name: 'Tom', age: 22};
var b = {name: 'Jack', height: 180};
$.extend(a, b);  // {name: 'Jack', age: 22, height: 180} - a 被修改

// 不修改原物件
var c = $.extend({}, a, b);  // a, b 不變,c 為合併結果

// 深度合併
var d = {user: {name: 'Tom', addr: {city: 'NY'}}};
var e = {user: {age: 25, addr: {zip: '10001'}}};
$.extend(true, d, e);  // d.user.addr = {city: 'NY', zip: '10001'} (合併巢狀物件)

擴展 jQuery 靜態工具方法

$.extend({
    // 命名空間模式
    mylib: {
        version: '1.0',
        log: function(msg) { console.log('[MYLIB]', msg); },
        formatDate: function(date, fmt) { /* ... */ }
    },
    // 獨立工具函數
    debounce: function(fn, delay) {
        var timer;
        return function() {
            clearTimeout(timer);
            timer = setTimeout(() => fn.apply(this, arguments), delay);
        };
    }
});

// 使用
$.mylib.log('Hello');
$.debounce(save, 300)();

開發 jQuery 插件:jQuery.fn.extend() 實戰

$.fn.extend({
    // 簡單插件:綠色高亮
    highlightGreen: function() {
        return this.css('background-color', '#c8f7c5');
    },
    
    // 完整插件模板:支援鏈式調用、選項合併、多元素
    tooltip: function(options) {
        // 1. 選項預設值合併
        var opts = $.extend({
            text: '提示內容',
            position: 'top',
            delay: 200,
            theme: 'dark'
        }, options);
        
        // 2. 返回 this 保持鏈式調用
        return this.each(function() {
            var $el = $(this);
            var timer;
            
            // 3. 事件綁定使用命名空間,便於銷毀
            $el.on('mouseenter.tooltip', function() {
                timer = setTimeout(function() {
                    showTooltip($el, opts);
                }, opts.delay);
            }).on('mouseleave.tooltip', function() {
                clearTimeout(timer);
                hideTooltip($el);
            });
            
            // 4. 儲存實例資料,支援銷毀
            $el.data('tooltip-opts', opts);
        });
    },
    
    // 銷毀方法
    destroyTooltip: function() {
        return this.each(function() {
            $(this).off('.tooltip').removeData('tooltip-opts');
            hideTooltip($(this));
        });
    }
});

// 使用範例
$('.btn-help').tooltip({text: '點擊查看幫助', position: 'right'});
$('#special').tooltip({theme: 'light', delay: 0});
// 銷毀
$('.btn-help').destroyTooltip();

jQuery 內部擴展自身的例子 (源碼片段)

jQuery.extend({
    noConflict: function(deep) { /* ... */ },
    isReady: false,
    readyWait: 1,
    // 核心工具函數
    each: function(obj, callback) { /* ... */ },
    map: function(elems, callback) { /* ... */ },
    grep: function(elems, callback) { /* ... */ },
    // 類型檢測
    isFunction: function(obj) { return jQuery.type(obj) === "function"; },
    isArray: Array.isArray || function(obj) { return jQuery.type(obj) === "array"; },
    isPlainObject: function(obj) { /* ... */ },
    // ... 更多
});

常見坑與最佳實踐

問題原因解決
陣列被替換而非合併深度合併視陣列為原子值手動合併陣列:target.arr = [...target.arr, ...src.arr]
原物件被意外修改淺層合併直接修改 target用 $.extend({}, target, src) 建立新物件
undefined 不覆蓋現有值源碼 copy !== undefined 判斷需明確刪除:delete target.key 或設為 null
循環引用導致堆疊溢位防護邏輯 target === copy 過於簡單避免共享引用結構,或使用 Lodash _.mergeWith 自定義合併
插件污染 jQuery 原型直接賦值 $.fn.xxx = ...統一用 $.fn.extend({xxx: ...}),便於管理

現代替代方案

ES6+ 環境下,原生提供更強大的物件合併:

// 淺層合併 (ES2018)
const merged = {...obj1, ...obj2, ...obj3};

// 深度合併:需自行實現或使用 Lodash
import { merge } from 'lodash-es';
const deepMerged = merge({}, obj1, obj2);

// Object.assign (ES2015) - 淺層,修改目標物件
Object.assign(target, src1, src2);

但 jQuery.extend 在舊瀏覽器相容性、插件生態、非標準行為 (如 undefined 不覆蓋) 仍有獨特價值。

總結

  • jQuery.extend():擴展類別/靜態方法,this === jQuery,用於工具函數、命名空間、內部核心方法
  • jQuery.fn.extend():擴展實例方法 (插件),this === jQuery.prototype,用於 $(selector).plugin() 形式
  • 核心邏輯統一:透過 arguments 處理多種參數組合,deep 旗標控制遞歸合併,isPlainObject/isArray 判斷型別保持
  • 插件開發鐵律:返回 this 保持鏈式、使用 .each() 遍歷元素、選項用 $.extend(defaults, options) 合併、事件命名空間便於銷毀、資料用 .data() 儲存

深入理解這套機制,不僅能讀懂 jQuery 核心與插件源碼,更能在維護舊專案、開發內部組件庫時遊刃有餘。雖然現代前端框架 (Vue/React/Svelte) 已改變開發範式,但 jQuery 的擴展設計思想——開放封裝、參數靈活、向後相容——依然值得借鑒。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved