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.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;
};
arguments 與 deep 旗標支援多種呼叫形式:
// 單參數:擴展 jQuery 自身 (target = this)
$.extend({ myUtil: function() {} });
// 兩參數:淺層合併 target <- src
$.extend(target, src);
// 三參數:深度合併
$.extend(true, target, src1, src2);
// 目標為空物件,不修改原物件
var result = $.extend({}, obj1, obj2);
length === i 判斷當只傳入來源物件時 (如 $.extend({a:1})),length=1, i=1,滿足條件,將 target=this (即 jQuery 或 jQuery.fn),實現「擴展自身」語義。
target === copy防止 o = {}; o.self = o; $.extend(true, o, {other: o}) 導致無限遞歸。但此檢查較粗略,可能誤判合法共享引用 (見文中 obj2={a:obj1} 例子)。
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} }
// 注意:陣列被整體替換,非合併;物件屬性合併
陣列視為原子值替換,物件才遞歸合併。這是設計決策,避免陣列索引合併產生不可預期結果。
$.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'} (合併巢狀物件)
$.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.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.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 不覆蓋) 仍有獨特價值。
this === jQuery,用於工具函數、命名空間、內部核心方法this === jQuery.prototype,用於 $(selector).plugin() 形式arguments 處理多種參數組合,deep 旗標控制遞歸合併,isPlainObject/isArray 判斷型別保持this 保持鏈式、使用 .each() 遍歷元素、選項用 $.extend(defaults, options) 合併、事件命名空間便於銷毀、資料用 .data() 儲存深入理解這套機制,不僅能讀懂 jQuery 核心與插件源碼,更能在維護舊專案、開發內部組件庫時遊刃有餘。雖然現代前端框架 (Vue/React/Svelte) 已改變開發範式,但 jQuery 的擴展設計思想——開放封裝、參數靈活、向後相容——依然值得借鑒。