jQuery extend()方法使用和實現
一、jQuery 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例項方法)
簡單理解兩者區別:
jQuery.extend(object); 為擴充套件jQuery類本身,為自身新增新的方法。
jQuery.fn.extend(object);給jQuery物件新增方法。
二、jQuery extend方法使用
1、jQuery.extend(object);
(a) jQuery.extend( target [, object1 ] [, objectN ] )
合併object1, objectN到target物件,如果只有一個引數,則該target物件會被合併到jQuery物件中,如下程式碼:
1. var object1 = {
2.
3. banana: { weight: 52, price: 100 },
4. cherry: 97
5. };
6. var object2 = {
7. banana: { price: 200 },
8. durian: 100
9. };
10.
11.// Merge object2 into object1
12.$.extend( object1, object2 );
13.console.log(object1.durian); //100
14.
15.// Merge object1 into jQuery
16.$.extend( object1 );
17.
(2) jQuery.extend( [deep ], target, object1 [, objectN ] )
深度複製合併物件,第一個引數是boolean型別的true時,將object1, objectN深度複製後合併到target中;關於深度複製,是將除null, undefined,window物件,dom物件,通過繼承建立的物件外的其它物件克隆後儲存到target中;
所排除的物件,一是考慮效能,二是考慮複雜度(例如dom及window物件,如果克隆複製,消耗過大,而通過繼承實現的物件,複雜程度不可預知,因此也不進行深度複製);
深度與非深度複製區別是,深度複製的物件中如果有複雜屬性值(如陣列、函式、json物件等),那將會遞迴屬性值的複製,合併後的物件修改屬性值不影響原物件,如下面例子:
1. obj1 = { a : 'a', b : 'b' };
2. obj2 = { x : { xxx : 'xxx', yyy : 'yyy' }, y : 'y' };
3. $.extend(true, obj1, obj2);
4. alert(obj1.x.xxx); // 得到"xxx"
5. obj2.x.xxx = 'zzz'; //修改obj2物件屬性的內聯值,不影響合併後物件obj1
6. alert(obj2.x.xxx); // 得到"zzz"
7. alert(obj1.x.xxx); // 得到"xxx" //值保持;如果不加true,則得到“zzz”
後面分析原始碼時,可以看到具體為什麼……
2、jQuery.fn.extend(object);
jQuery.fn = jQuery.prototype 即指向jQuery物件的原型鏈,對其它進行的擴充套件,作用在jQuery物件上面;一般用此方法來擴充套件jQuery的物件外掛
1. //將hello方法合併到jquery的例項物件中。
2. $.fn.extend({
3. hello:function(){alert('hello');}
4. });
5.
6. //在jquery全域性物件中擴充套件一個net名稱空間。
7. $.extend($.net,{
8. hello:function(){alert('hello');}
9. }); //使用jQuery.net.hello();
二、jQuery extend實現原理
extend()函式是jQuery的基礎函式之一,作用是擴充套件現有的物件。例如下面的程式碼:
1. <script type="text/javascript" src="jquery-1.5.2.js"></script>
2. <script>
3. obj1 = { a : 'a', b : 'b' };
4. obj2 = { x : { xxx : 'xxx', yyy : 'yyy' }, y : 'y' };
5.
6. $.extend(true, obj1, obj2);
7.
8. alert(obj1.x.xxx); // 得到"xxx"
9.
10.obj2.x.xxx = 'zzz';
11.alert(obj2.x.xxx); // 得到"zzz"
12.alert(obj1.x.xxx); // 得帶"xxx"
13.</script>
$.extend(true, obj1, obj2)表示以obj2中的屬性擴充套件物件obj1,第一個引數設為true表示深複製。
雖然obj1中原來沒有"x"屬性,但經過擴充套件後,obj1不但具有了"x"屬性,而且對obj2中的"x"屬性的修改也不會影響到obj1中"x"屬性的值,這就是所謂的“深複製”了。
1、淺複製的實現
如果僅僅需要實現淺複製,可以採用類似下面的寫法:
1. $ = {
2. extend : function(target, options) {
3. for (name in options) {
4. target[name] = options[name];
5. }
6. return target;
7. }
8. };
也就是簡單地將options中的屬性複製到target中。我們仍然可以用類似的程式碼進行測試,但得到的結果有所不同(假設我們的js命名為“jquery-extend.js”):
1. <script type="text/javascript" src="jquery-extend.js"></script>
2. <script>
3. obj1 = { a : 'a', b : 'b' };
4. obj2 = { x : { xxx : 'xxx', yyy : 'yyy' }, y : 'y' };
5.
6. $.extend(obj1, obj2);
7.
8. alert(obj1.x.xxx); // 得到"xxx"
9.
10.obj2.x.xxx = 'zzz';
11.alert(obj2.x.xxx); // 得到"zzz"
12.alert(obj1.x.xxx); // 得帶"zzz"
13.</script>
obj1中具有了"x"屬性,但這個屬性是一個物件,對obj2中的"x"的修改也會影響到obj1,這可能會帶來難以發現的錯誤。
2、深複製的實現
如果我們希望實現“深複製”,當所複製的物件是陣列或者物件時,就應該遞迴呼叫extend。如下程式碼是“深複製”的簡單實現:
1. $ = {
2. extend : function(deep, target, options) {
3. for (name in options) {
4. copy = options[name];
5. if (deep && copy instanceof Array) {
6. target[name] = $.extend(deep, [], copy);
7. } else if (deep && copy instanceof Object) {
8. target[name] = $.extend(deep, {}, copy);
9. } else {
10.target[name] = options[name];
11.}
12.}
13.return target;
14.}
15.};
具體分為三種情況:
1. 屬性是陣列時,則將target[name]初始化為空陣列,然後遞迴呼叫extend;
2. 屬性是物件時,則將target[name]初始化為空物件,然後遞迴呼叫extend;
3. 否則,直接複製屬性。
測試程式碼如下:
1. <script type="text/javascript" src="jquery-extend.js"></script>
2. <script>
3. obj1 = { a : 'a', b : 'b' };
4. obj2 = { x : { xxx : 'xxx', yyy : 'yyy' }, y : 'y' };
5. $.extend(true, obj1, obj2);
6. alert(obj1.x.xxx); // 得到"xxx"
7. obj2.x.xxx = 'zzz';
8. alert(obj2.x.xxx); // 得到"zzz"
9. alert(obj1.x.xxx); // 得到"xxx"
10.</script>
現在如果指定為深複製的話,對obj2的修改將不會對obj1產生影響了;不過這個程式碼還存在一些問題,比如“instanceof Array”在IE5中可能存在不相容的情況。jQuery中的實現實際上會更復雜一些。
3、更完整的實現
下面的實現與jQuery中的extend()會更接近一些:
11.$ = function() {
12.var copyIsArray,
13.toString = Object.prototype.toString,
14.hasOwn = Object.prototype.hasOwnProperty;
15.
16.class2type = {
17.'[object Boolean]' : 'boolean',
18.'[object Number]' : 'number',
19.'[object String]' : 'string',
20.'[object Function]' : 'function',
21.'[object Array]' : 'array',
22.'[object Date]' : 'date',
23.'[object RegExp]' : 'regExp',
24.'[object Object]' : 'object'
25.},
26.
27.type = function(obj) {
28.return obj == null ? String(obj) : class2type[toString.call(obj)] || "object";
29.},
30.
31.isWindow = function(obj) {
32.return obj && typeof obj === "object" && "setInterval" in obj;
33.},
34.
35.isArray = Array.isArray || function(obj) {
36.return type(obj) === "array";
37.},
38.
39.isPlainObject = function(obj) {
40.if (!obj || type(obj) !== "object" || obj.nodeType || isWindow(obj)) {
41.return false;
42.}
43.
44.if (obj.constructor && !hasOwn.call(obj, "constructor")
45.&& !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) {
46.return false;
47.}
48.
49.var key;
50.for (key in obj) {
51.}
52.
53.return key === undefined || hasOwn.call(obj, key);
54.},
55.
56.extend = function(deep, target, options) {
57.for (name in options) {
58.src = target[name];
59.copy = options[name];
60.
61.if (target === copy) { continue; }
62.
63.if (deep && copy
64.&& (isPlainObject(copy) || (copyIsArray = isArray(copy)))) {
65.if (copyIsArray) {
66.copyIsArray = false;
67. clone = src && isArray(src) ? src : [];
68.
69.} else {
70.clone = src && isPlainObject(src) ? src : {};
71.}
72.
73.target[name] = extend(deep, clone, copy);
74.} else if (copy !== undefined) {
75.target[name] = copy;
76.}
77.}
78.
79.return target;
80.};
81.
82.return { extend : extend };
83.}();
首先是 $ = function(){...}();這種寫法,可以理解為與下面的寫法類似:
1. func = function(){...};
2. $ = func();
也就是立即執行函式,並將結果賦給$。這種寫法可以利用function來管理作用域,避免區域性變數或區域性函式影響全域性域。另外,我們只希望使用者呼叫$.extend(),而將內部實現的函式隱藏,因此最終返回的物件中只包含extend:
1. return { extend : extend };
接下來,我們看看extend函式與之前的區別,首先是多了這句話:
1. if (target === copy) { continue; }
這是為了避免無限迴圈,要複製的屬性copy與target相同的話,也就是將“自己”複製為“自己的屬性”,可能導致不可預料的迴圈。
然後是判斷物件是否為陣列的方式:
1. type = function(obj) {
2. return obj == null ? String(obj) : class2type[toString.call(obj)] || "object";
3. },
4. isArray = Array.isArray || function(obj) {
5. return type(obj) === "array";
6. }
如果瀏覽器有內建的Array.isArray實現,就使用瀏覽器自身的實現方式,否則將物件轉為String,看是否為"[object Array]"。
最後逐句地看看isPlainObject的實現:
1. if (!obj || type(obj) !== "object" || obj.nodeType || isWindow(obj)) {
2. return false;
3. }
如果定義了obj.nodeType,表示這是一個DOM元素;這句程式碼表示以下四種情況不進行深複製:
1. 物件為undefined;
2. 轉為String時不是"[object Object]";
3. obj是一個DOM元素;
4. obj是window。
之所以不對DOM元素和window進行深複製,可能是因為它們包含的屬性太多了;尤其是window物件,所有在全域性域宣告的變數都會是其屬性,更不用說內建的屬性了。
接下來是與建構函式相關的測試:
1. if (obj.constructor && !hasOwn.call(obj, "constructor")
2. && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) {
3. return false;
4. }
如果物件具有建構函式,但卻不是自身的屬性,說明這個建構函式是通過prototye繼承來的,這種情況也不進行深複製。這一點可以結合下面的程式碼結合進行理解:
1. var key;
2. for (key in obj) {
3. }
4.
5. return key === undefined || hasOwn.call(obj, key);
這幾句程式碼是用於檢查物件的屬性是否都是自身的,因為遍歷物件屬性時,會先從自身的屬性開始遍歷,所以只需要檢查最後的屬性是否是自身的就可以了。
這說明如果物件是通過prototype方式繼承了建構函式或者屬性,則不對該物件進行深複製;這可能也是考慮到這類物件可能比較複雜,為了避免引入不確定的因素或者為複製大量屬性而花費大量時間而進行的處理,從函式名也可以看出來,進行深複製的只有"PlainObject"。
如果我們用如下程式碼進行測試:
1. <script type="text/javascript" src="jquery-1.5.2.js"></script>
2. <script>
3. function O() {
4. this.yyy = 'yyy';
5. }
6.
7. function X() {
8. this.xxx = 'xxx';
9. }
10.
11.X.prototype = new O();
12.
13.x = new X();
14.
15.obj1 = { a : 'a', b : 'b' };
16.obj2 = { x : x };
17.$.extend(true, obj1, obj2);
18.
19.alert(obj1.x.yyy); // 得到"xxx"
20.obj2.x.yyy = 'zzz';
21.alert(obj1.x.yyy); // 得到"zzz"
22.</script>
可以看到,這種情況是不進行深複製的。
總之,jQuery中的extend()的實現方式,考慮了相容瀏覽器的相容,避免效能過低,和避免引入不可預料的錯誤等因素。
三、jQuery原始碼實現
還是先加一個例子,區別jQuery.extend及jQuery.fn.extend:
1. jQuery.extend({
2. sayhello:function(){
3. console.log("Hello,This is jQuery Library");
4. }
5. })
6. $.sayhello(); //Hello, This is jQuery Library
7.
8. jQuery.fn.extend({
9. check: function() {
10.return this.each(function() {
11.this.checked = true;
12.});
13.},
14.uncheck: function() {
15.return this.each(function() {
16.this.checked = false;
17.});
18.}
19.})
20.$( "input[type='checkbox']" ).check(); //所有的checkbox都會被選擇
1、extend無註釋的原始碼
檔案如下
1. jQuery.extend = jQuery.fn.extend = function() {
2. var options, name, src, copy, copyIsArray, clone,
3. target = arguments[0] || {},
4. i = 1,
5. length = arguments.length,
6. deep = false;
7.
8. // Handle a deep copy situation
9. if ( typeof target === "boolean" ) {
10.deep = target;
11.target = arguments[1] || {};
12.// skip the boolean and the target
13.i = 2;
14.}
15.
16.// Handle case when target is a string or something (possible in deep copy)
17.if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
18.target = {};
19.}
20.
21.// extend jQuery itself if only one argument is passed
22.if ( length === i ) {
23.target = this;
24.--i;
25.}
26.
27.for ( ; i < length; i++ ) {
28.// Only deal with non-null/undefined values
29.if ( (options = arguments[ i ]) != null ) {
30.// Extend the base object
31.for ( name in options ) {
32.src = target[ name ];
33.copy = options[ name ];
34.
35.// Prevent never-ending loop
36.if ( target === copy ) {
37.continue;
38.}
39.
40.// Recurse if we're merging plain objects or arrays
41.if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
42.if ( copyIsArray ) {
43.copyIsArray = false;
44.clone = src && jQuery.isArray(src) ? src : [];
45.
46.} else {
47.clone = src && jQuery.isPlainObject(src) ? src : {};
48.}
49.
50.// Never move original objects, clone them
51.target[ name ] = jQuery.extend( deep, clone, copy );
52.
53.// Don't bring in undefined values
54.} else if ( copy !== undefined ) {
55.target[ name ] = copy;
56.}
57.}
58.}
59.}
60.
61.// Return the modified object
62.return target;
63.};
程式碼的大部分都是用來實現jQuery.extend()中有多個引數時的物件合併,深度拷貝問題,如果去掉這些功能,讓extend只有擴充套件靜態和例項方法的功能,那麼程式碼如下:
1. jQuery.extend = jQuery.fn.extend = function(obj){
2. //obj是傳遞過來擴充套件到this上的物件
3. var target=this;
4. for (var name in obj){
5. //name為物件屬性
6. //copy為屬性值
7. copy=obj[name];
8. //防止迴圈呼叫
9. if(target === copy) continue;
10.//防止附加未定義值
11.if(typeof copy === 'undefined') continue;
12.//賦值
13.target[name]=copy;
14.}
15.return target;
16.}
2、extend方法進行註釋解釋:
1. jQuery.extend = jQuery.fn.extend = function() {
2. // 定義預設引數和變數
3. // 物件分為擴充套件物件和被擴充套件的物件
4. //options 代表擴充套件的物件中的方法
5. //name 代表擴充套件物件的方法名
6. //i 為擴充套件物件引數起始值
7. //deep 預設為淺複製
8. var options, name, src, copy, copyIsArray, clone,
9. target = arguments[0] || {},
10.i = 1,
11.length = arguments.length,
12.deep = false;
13.
14.//當第一個引數為布林型別是,次引數定義是否為深拷貝
15.//對接下來的引數進行處理
16.if ( typeof target === "boolean" ) {
17.deep = target;
18.target = arguments[1] || {};
19.// 當定義是否深拷貝時,引數往後移動一位
20.i = 2;
21.}
22.
23.// 如果要擴充套件的不是物件或者函式,則定義要擴充套件的物件為空
24.if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
25.target = {};
26.}
27.
28.// 當只含有一個引數時,被擴充套件的物件是jQuery或jQuery.fn
29.if ( length === i ) {
30.target = this;
31.--i;
32.}
33.
34.//對從i開始的多個引數進行遍歷
35.for ( ; i < length; i++ ) {
36.// 只處理有定義的值
37.if ( (options = arguments[ i ]) != null ) {
38.// 展開擴充套件物件
39.for ( name in options ) {
40.src = target[ name ];
41.copy = options[ name ];
42.
43.// 防止迴圈引用
44.if ( target === copy ) {
45.continue;
46.}
47.
48.// 遞迴處理深拷貝
49.if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
50.if ( copyIsArray ) {
51.copyIsArray = false;
52.clone = src && jQuery.isArray(src) ? src : [];
53.
54.} else {
55.clone = src && jQuery.isPlainObject(src) ? src : {};
56.}
57.
58.target[ name ] = jQuery.extend( deep, clone, copy );
59.
60.// 不處理未定義值
61.} else if ( copy !== undefined ) {
62.//給target增加屬性或方法
63.target[ name ] = copy;
64.}
65.}
66.}
67.}
68.
69.//返回
70.return target;
71.};