jQuery中的extend()方法
通常我们使用jquery的extend时,大都是为了实现默认字段的覆盖,即若传入某个字段的值,则使用传入值,否则使用默认值。如下面的代码:
1 function getOpt(option){ 2 var _default = { 3 name : 'wenzi', 4 age : '25', 5 sex : 'male' 6 } 7 $.extend(_default, option); 8 return _default; 9 } 10 getOpt(); // {name: "wenzi", age: "25", sex: "male"} 11 getOpt({name:'bing'}); // {name: "bing", age: "25", sex: "male"} 12 getOpt({name:'bing', age:36, sex:'female'}); // {name: "bing", age: 36, sex: "female"}
那现在我们就得需要知道这个 extend 具体是怎么实现的了,除了实现上面的功能,还有其他作用么?那肯定是有的啦,否则我也不会问那句话了((⊙﹏⊙)b)。我们先来看看 extend 主要有哪些功能,然后再看实现这些功能的原理。
1. extend能实现的功能
其实从 extend 的含义里,我们就能知道 extend 是做什么的了。 extend 翻译成汉语后就是:延伸、扩展、推广。
1.1 将两个或更多对象的内容合并到第一个对象
我们来看看 $.extend() 提供的参数: jQuery.extend( target [, object1 ] [, objectN ] ) , extend 方法需要至少传入一个参数,第一个必需,后面的都是可选参数。若传给 extend 是两个或两个以上的参数都是对象类型,那么就会把后面所有对象的内容合并给 target (第一个对象)上。
我们再来看看上面的例子:
1 function getOpt(option){ 2 var _default = { 3 name : 'wenzi', 4 age : '25', 5 sex : 'male' 6 } 7 `$.extend(_default, option);` 8 return _default; 9 }
$.extend() 中接收了两个参数 _default 和 option ,那么 extend 方法执行时,就会把 option 对象上字段的值全给了 _default 。于是 _default 上设置的默认值就会被option上的值覆盖。当然,若option上没有这个字段,就不会覆盖_default上字段的值。
上面函数中的extend,只是传入了两个参数,那传的参数再更多一些呢:
1 function getOpt(target, obj1, obj2, obj3){ 2 $.extend(target, obj1, obj2, obj3); 3 return target; 4 } 5 6 var _default = { 7 name : 'wenzi', 8 age : '25', 9 sex : 'male' 10 } 11 var obj1 = { 12 name : 'obj1' 13 } 14 var obj2 = { 15 name : 'obj2', 16 age : '36' 17 } 18 var obj3 = { 19 age : '67', 20 sex : {'error':'sorry, I dont\'t kown'} 21 } 22 getOpt(_default, obj1, obj2, obj3); // {name: "obj2", age: "67", sex: {error: "sorry, I dont't kown"}}
这里我们传入了4个参数,然后 getOpt() 返回第一个参数的值。从运行的得到结果我们可以看到,属性值永远是最后一个属性的值。
还有很重要的一点, $.extend() 其实是有返回值的,返回的就是修改后的第一个参数的值。如我们可以把上面的函数修改成这样:
1 function getOpt(target, obj1, obj2, obj3){ 2 var result = $.extend(target, obj1, obj2, obj3); 3 return result; // // result即修改后的target值 4 }
若我们传入的参数不想被修改,我们可以用一个空对象来作为第一个参数,然后获取$.extend()
的返回值:
1 function getOpt(target, obj1, obj2, obj3){ 2 var result = $.extend({}, target, obj1, obj2, obj3); 3 return result; // // result即为{}修改后的值 4 }
1.2 为JQUERY扩展方法或属性
刚才我们在1.1中讲的 $.extend() 的例子都是传了两个或两个以上的参数,但其实只有一个参数是必须的。若只传一个参数会怎样呢。
如果只有一个参数提供给 $.extend() ,这意味着目标参数被省略。在这种情况下, jQuery 对象本身被默认为目标对象。这样,我们可以在 jQuery 的命名空间下添加新的功能。这对于插件开发者希望向 jQuery 中添加新函数时是很有用的。
1 $.extend({ 2 _name : 'wenzi', 3 _getName : function(){ 4 return this._name; 5 } 6 }) 7 8 $._name; // wenzi 9 $._getName(); // wenzi
这样我们就为jQuery扩展了 _name 属性和 _getName 方法。
1.3 深度拷贝和浅度拷贝
针对什么是深度拷贝,什么是浅度拷贝,我们先来看一个简单的例子。
1 var obj = {name:'wenzi', sex:'male'}; 2 var obj1 = obj; // 赋值 3 obj1.name = 'bing'; 4 console.log(obj.name); // bing
我们修改了obj1中的name值,结果obj中的值也跟着发生了变化,这是为什么呢。其实这就是浅度拷贝
:这仅仅是将obj对象的引用地址简单的复制了一份给予变量 obj1,而并不是将真正的对象克隆了一份,因此obj和obj1指向的都是同一个地址。当修改obj1的属性或给obj1添加新属性时,obj都会受到影响。
可是如果变量的值不是对象和数组,修改后面的变量是不会影响到前面的变量:
1 var s = 'hello'; 2 var t = s; 3 t = 'world'; 4 console.log(s); // hello
那么深度拷贝就不是拷贝引用地址,而是实实在在的复制一份新对象给新的变量。 在上面使用$.extend()
中,都是使用的浅度拷贝,因此若后面的参数值是object类型或array类型,修改_default(target)的值,就会影响后面参数的值。
如我们使用 getOpt(_default, obj1, obj2, obj3) ;
得到的 _default 值是 {name: “obj2”, age: “67”, sex: {error: “sorry, I dont’t kown”}} ,可是若:
_default.sex.error = 'hello world';
那么 obj3.sex.error 也会跟着修改,因为 obj3.sex 是一个 object 类型。
不过 $.extend() 也提供了深度拷贝的方法: jQuery.extend( [deep ], target, object1 [, objectN ] ) 。若第一个参数是 boolean 类型,且值是 true ,那么就会把第二个参数作为目标参数进行合并。
1 var obj = {name:'wenzi', score:80}; 2 var obj1 = {score:{english:80, math:90}} 3 $.extend(true, obj, obj1); 4 obj.score.english = 10; 5 console.log(obj.score.english); // 10 6 console.log(obj1.score.english); // 80
执行后我们发现,无论怎么修改obj.score里的值,都不会影响到obj1.score了。
2. jQuery中extend实现原理
其实不看源码,对 extend 大致的过程应该也是了解的:对后一个参数进行循环,然后把后面参数上所有的字段都给了第一个字段,若第一个参数里有相同的字段,则进行覆盖操作,否则就添加一个新的字段。
下面是jQuery中关于 extend 的源码,我就在源码上进行注释讲解了,随后再在后面进行总结:
1 // 为与源码的下标对应上,我们把第一个参数称为`第0个参数`,依次类推 2 jQuery.extend = jQuery.fn.extend = function() { 3 var options, name, src, copy, copyIsArray, clone, 4 target = arguments[0] || {}, // 默认第0个参数为目标参数 5 i = 1, // i表示从第几个参数凯斯想目标参数进行合并,默认从第1个参数开始向第0个参数进行合并 6 length = arguments.length, 7 deep = false; // 默认为浅度拷贝 8 9 // 判断第0个参数的类型,若第0个参数是boolean类型,则获取其为true还是false 10 // 同时将第1个参数作为目标参数,i从当前目标参数的下一个 11 // Handle a deep copy situation 12 if ( typeof target === "boolean" ) { 13 deep = target; 14 15 // Skip the boolean and the target 16 target = arguments[ i ] || {}; 17 i++; 18 } 19 20 // 判断目标参数的类型,若目标参数既不是object类型,也不是function类型,则为目标参数重新赋值 21 // Handle case when target is a string or something (possible in deep copy) 22 if ( typeof target !== "object" && !jQuery.isFunction(target) ) { 23 target = {}; 24 } 25 26 // 若目标参数后面没有参数了,如$.extend({_name:'wenzi'}), $.extend(true, {_name:'wenzi'}) 27 // 则目标参数即为jQuery本身,而target表示的参数不再为目标参数 28 // Extend jQuery itself if only one argument is passed 29 if ( i === length ) { 30 target = this; 31 i--; 32 } 33 34 // 从第i个参数开始 35 for ( ; i < length; i++ ) { 36 // 获取第i个参数,且该参数不为null和undefind,在js中null和undefined,如果不区分类型,是相等的,null==undefined为true, 37 // 因此可以用null来同时过滤掉null和undefind 38 // 比如$.extend(target, {}, null);中的第2个参数null是不参与合并的 39 // Only deal with non-null/undefined values 40 if ( (options = arguments[ i ]) != null ) { 41 42 // 使用for~in获取该参数中所有的字段 43 // Extend the base object 44 for ( name in options ) { 45 src = target[ name ]; // 目标参数中name字段的值 46 copy = options[ name ]; // 当前参数中name字段的值 47 48 // 若参数中字段的值就是目标参数,停止赋值,进行下一个字段的赋值 49 // 这是为了防止无限的循环嵌套,我们把这个称为,在下面进行比较详细的讲解 50 // Prevent never-ending loop 51 if ( target === copy ) { 52 continue; 53 } 54 55 // 若deep为true,且当前参数中name字段的值存在且为object类型或Array类型,则进行深度赋值 56 // Recurse if we're merging plain objects or arrays 57 if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { 58 // 若当前参数中name字段的值为Array类型 59 // 判断目标参数中name字段的值是否存在,若存在则使用原来的,否则进行初始化 60 if ( copyIsArray ) { 61 copyIsArray = false; 62 clone = src && jQuery.isArray(src) ? src : []; 63 64 } else { 65 // 若原对象存在,则直接进行使用,而不是创建 66 clone = src && jQuery.isPlainObject(src) ? src : {}; 67 } 68 69 // 递归处理,此处为2.2 70 // Never move original objects, clone them 71 target[ name ] = jQuery.extend( deep, clone, copy ); 72 73 // deep为false,则表示浅度拷贝,直接进行赋值 74 // 若copy是简单的类型且存在值,则直接进行赋值 75 // Don't bring in undefined values 76 } else if ( copy !== undefined ) { 77 // 若原对象存在name属性,则直接覆盖掉;若不存在,则创建新的属性 78 target[ name ] = copy; 79 } 80 } 81 } 82 } 83 84 // 返回修改后的目标参数 85 // Return the modified object 86 return target; 87 };
源码分析完了,下面我们来讲解下源码中存在的几个难点和重点。
2.1 若参数中字段的值就是目标参数,停止赋值
在源码中进行了一下这样的判断:
1 // Prevent never-ending loop 2 if ( target === copy ) { 3 continue; 4 }
为什么要有这样的判断,我们来看一个简单的例子,如果没有这个判断会怎么样:
1 var _default = {name : 'wenzi'}; 2 var obj = {name : _default} 3 $.extend(_default, obj); 4 console.log(_default);
输出的_default是什么呢:
1 _default = {name : _default};
_default是object类型,里面有个字段name,值是_default,而_default是object类型,里面有个字段name,值是_default……,无限的循环下去。于是jQuery中直接不进行操作,跳过这个字段,进行下一个字段的操作。
2.2 深度拷贝时进行递归处理
我们在前面稍微的讲解了一下,变量值为简单类型(如number, string, boolean)进行赋值时是不会影响上一个变量的值的,因此,如果当前字段的值为 Object 或 Array 类型,需要对其进行拆分,直到字段的值为简单类型(如number, string, boolean)时才进行赋值操作。
3. $.extend()与$.fn.extend()
上面讲解的全都是 $.extend() ,根本就没讲 $.fn.extend() 。可是,你有没有发现一个细节,在这段代码的第一行是怎么写的:
jQuery.extend = jQuery.fn.extend = function(){}
也就是说 $.extend() 与 $.fn.extend() 共用的是同一个函数体,所有的操作都是一样的,只不过两个 extend 使用的对象不同罢了: $.extend() 是在 jQuery($) 上进行操作的;而 $.fn.extend() 是在jQuery对象上进行操作的,如 $(‘div’).extend() 。
4. 总结
这就是jQuery中extend的实现,以后若我们需要用到上面的功能时,除了使用$.extend(),我们也可以在不引入jQuery框架的情况下,自己写一个简单的extend()来实现上面的功能。
原文地址:http://www.xiabingbao.com/jquery/2015/05/30/jquery-extend.html