一、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对象中,如下代码:
var object1 = { 2. apple: 0, 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); // 14. 15.// Merge object1 into jQuery 16.$.extend( object1 ); 17.console.log( $.apple ); //
(2) jQuery.extend( [deep ], target, object1 [, objectN ] )
深度复制合并对象,第一个参数是boolean类型的true时,将object1, objectN深度复制后合并到target中;关于深度复制,是将除null,undefined,window对象,dom对象,通过继承创建的对象外的其它对象克隆后保存到target中;
所排除的对象,一是考虑性能,二是考虑复杂度(例如dom及window对象,如果克隆复制,消耗过大,而通过继承实现的对象,复杂程度不可预知,因此也不进行深度复制);
深度与非深度复制区别是,深度复制的对象中如果有复杂属性值(如数组、函数、json对象等),那将会递归属性值的复制,合并后的对象修改属性值不影响原对象,如下面例子:
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的对象插件
//将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的基础函数之一,作用是扩展现有的对象。例如下面的代码:
<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、浅复制的实现
如果仅仅需要实现浅复制,可以采用类似下面的写法:
$ = { 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”):
<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。如下代码是“深复制”的简单实现:
$ = { 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. 否则,直接复制属性。
测试代码如下:
<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()会更接近一些:
$ = 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(){...}();这种写法,可以理解为与下面的写法类似:
func = function(){...}; 2. $ = func();
也就是立即执行函数,并将结果赋给$。这种写法可以利用function来管理作用域,避免局部变量或局部函数影响全局域。另外,我们只希望使用者调用$.extend(),而将内部实现的函数隐藏,因此最终返回的对象中只包含extend:
return { extend : extend };
接下来,我们看看extend函数与之前的区别,首先是多了这句话:
if (target === copy) { continue; }
这是为了避免无限循环,要复制的属性copy与target相同的话,也就是将“自己”复制为“自己的属性”,可能导致不可预料的循环。
然后是判断对象是否为数组的方式:
type = function(obj) {
return obj == null ? String(obj) : class2type[toString.call(obj)] || "object";
}, isArray = Array.isArray || function(obj) { return type(obj) === "array";
}
如果浏览器有内置的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.};