文章主要分为三部分,第一部分简单介绍了extend
的语法,第二部分经过实例介绍extend
的用途,最后一部分是extend
的源码解读,同时另附extend
的另外一种实现方式。html
jQuery 的 API 手册中,extend 方法挂载在 jQuery 和 jQuery.fn 两个不一样的对象上,但在 jQuery 内部代码实现的是相同的,只是功能各不相同。jquery
官方解释:ajax
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 实例方法 。)json
syntax:segmentfault
jQuery.extend([deep,] [target,] object1 [,objectN]);数组
jQuery.fn.extend([deep,] [target,] object1 [,objectN])ide
deep: Boolen类型,可选,表示是否进行递归合并(深/浅复制),为true是为深复制;默认值为false,浅复制。
target:扩展对象,可选,将接收新的属性。
objectN:一个对象,包含额外的属性,扩展到目标对象(扩展对象)。函数
在这种状况下,extend方法须要至少传入两个对象,语法以下:工具
jQuery.extend(target, object1 [,objectN]) or jQuery.fn.extend(target, object1 [,objectN])
合并object1,...,objectN对象内容到第一个对象target。
这里须要注意一下几点:
1.合并后target对象的内容会改变,若是不但愿改变target对象的内容,能够将第一个对象设置为
{}
.
2.这种方法是有返回值的,返回值就是修改后的target
对象.
3.合并后的target对象的内容,属性值永远是在object1,...,objectN几个对象中最后一次出现时的属性值,也就是对于相同名字的属性,后面对象中的属性值会覆盖前面对象的属性值。
实例
function getOpt(target, obj1, obj2, obj3){ $.extend(target, obj1, obj2, obj3); return target; } var _default = { name : 'wenzi', age : '25', sex : 'male' } var obj1 = { name : 'obj1' } var obj2 = { name : 'obj2', age : '36' } var obj3 = { age : '67', sex : {'error':'sorry, I dont\'t kown'} } getOpt(_default, obj1, obj2, obj3); // {name: "obj2", age: "67", sex: {error: "sorry, I dont't kown"}}
这条用法实际上就是用的“将两个或者更多个对象合并到第一个对象”,之因此把提出来另起一个标题,是由于这是一种很常见的编程技巧。
实例:
function getOpt(option){ var _default = { name : 'wenzi', age : '25', sex : 'male' } $.extend(_default, option); return _default; } getOpt(); // {name: "wenzi", age: "25", sex: "male"} getOpt({name:'bing'}); // {name: "bing", age: "25", sex: "male"} getOpt({name:'bing', age:36, sex:'female'}); // {name: "bing", age: 36, sex: "female"}
函数getOpt含有默认参数(对象)_default,若传入函数的参数option(对象)中含有某个属性的值,则使用传入值,不然使用默认值。
所谓的深浅拷贝,就是C语言中的拷贝地址与数据
浅复制对象A时,对象B将复制A的全部字段,若是字段是内存地址,B将复制地址,若果字段是基元类型,B将复制其值。
浅复制的缺点是若是你改变了对象B所指向的内存地址,你同时也改变了对象A指向这个地址的字段
function copy(target,cloneObj){ for(var i in cloneObj){ target[i] = cloneObj[i]; } return target; } var a = { a:{ c:"c" }, b:"b" } var t = {}; copy(t,a); t.a.c ="e"; console.log(a.a.c);//e
这种方式会彻底复制全部数据,优势是B与A不会相互依赖(A,B彻底脱离关联), 缺点是复制的速度慢,代价大。
一种是实现深度拷贝的方案:
function type(obj){ return Object.prototype.toString.call(obj).slice(8,-1); } function deepCopy(target,cloneObj){ var copy; for(var i in cloneObj){ copy = cloneObj[i]; if(target === copy){ continue; } if(type(copy) === "Array"){ target[i] = arguments.callee(target[i] || [],copy); }else if(type(copy) === "Object"){ target[i] = arguments.callee(target[i] || {},copy); }else{ target[i] = copy; } } return target; } var a = { a:{ c:"c" }, b:"b" } var t = deepCopy({},a); t.a.c ="e"; console.log(a.a.c);//c
注意关于arguments,caller,callee
不懂的请移步这里JavaScript 之arguments、caller 和 callee 介绍 .
能够看到a没有被修改,可是要更深层次的遍历,确定很耗费性能的。用for-in
把全部可枚举的包括原型链上的一块儿遍历了。
在用extend
方法进行对象合并时,能够指定第一个参数为boolean类型,来决定是深拷贝(true)仍是浅拷贝(false),语法以下:
jQuery.extend(deep, target, object1 [,objectN]) or jQuery.fn.extend(deep, target, object1 [,objectN])
实例:
var obj1 = { name: "John", location: { city: "Boston", county: "USA" } } var obj2 = { last: "Resig", location: { state: "MA", county: "China" } } $.extend(false, {}, obj1, obj2); // { name: "John", last: "Resig", location: { state: "MA", county: "China" }} $.extend(true, {}, obj1, obj2); // { name: "John", last: "Resig", location: { city: "Boston", state: "MA", county: "China" }}
因而可知,执行 深度复制 会递归遍历每一个对象中含有复杂对象(如:数组、函数、json对象等)的属性值进行复制,并且 浅度复制 便不会这么作。
jQuery插件开发分为两种:1 类级别、2 对象级别
类级别(类方法):是直接可使用类引用,不须要实例化就可使用的方法。通常在项目中 类方法 都是被设置为工具类使用;
对象级别(实例方法)必须先建立实例,而后才能经过实例调用该 实例方法
jQuery能够看作是这个封装得很是好的类,而咱们可使用jQuery选择器来建立 jQuery 的实例。好比:使 id 选择器$('#btn')来建立一个实例。
类级别你能够理解为拓展jQuery
类,最明显的例子是$.ajax(...)
,至关于静态方法,开发扩展其方法时使用$.extend
方法
实例1:
$.extend({ add:function(a,b){return a+b;} minus:function(a,b){return a-b;} }); 调用方式 var i = $.add(3,2); var j = $.minus(3,2);
对象级别则能够理解为基于对象的拓展,如$("#table").set(...)
; 这里这个set
呢,就是基于对象的拓展了。开发扩展其方法时使用$.fn.extend
方法,
$.fn.extend({ check:function(){ return this.each({ this.checked=true; }); }, uncheck:function(){ return this.each({ this.checked=false; }); } }); 调用方式 $('input[type=checkbox]').check(); $('input[type=checkbox]').uncheck();
相似于命名空间的扩展
$.xy = { add:function(a,b){ return a+b; } , minus:function(a,b){ return a-b; }, voidMethod:function(){ alert("void"); } }; 调用方式 var i = $.xy.add(3,2); var m = $.xy.minus(3,2); $.xy.voidMethod();
若是只有一个参数对象提供给$.extend()
,这意味着目标参数被省略。在这种状况下,调用extend方法的对象被默认为目标对象,参数对象中的内容将合并到目标对象中去。语法以下:
jQuery.extend(object) or jQuery.fn.extend(object)
该方法就是将src合并到jquery的全局对象中去,如:
$.extend({ hello:function(){alert('hello');} });
就是将hello方法合并到jquery的全局对象中。
该方法将src合并到jquery的实例对象中去,如:
$.fn.extend({ hello:function(){alert('hello');} });
就是将hello方法合并到jquery的实例对象中。
// 为与源码的下标对应上,咱们把第一个参数称为`第0个参数`,依次类推 jQuery.extend = jQuery.fn.extend = function() { var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {}, // 默认第0个参数为目标参数 i = 1, // i表示从第几个参数凯斯想目标参数进行合并,默认从第1个参数开始向第0个参数进行合并 length = arguments.length, deep = false; // 默认为浅度拷贝 // 判断第0个参数的类型,若第0个参数是boolean类型,则获取其为true仍是false // 同时将第1个参数做为目标参数,i从当前目标参数的下一个 // Handle a deep copy situation if ( typeof target === "boolean" ) { deep = target; // Skip the boolean and the target target = arguments[ i ] || {}; i++; } // 判断目标参数的类型,若目标参数既不是object类型,也不是function类型,则为目标参数从新赋值 // Handle case when target is a string or something (possible in deep copy) if ( typeof target !== "object" && !jQuery.isFunction(target) ) { target = {}; } // 若目标参数后面没有参数了,如$.extend({_name:'wenzi'}), $.extend(true, {_name:'wenzi'}) // 则目标参数即为jQuery自己,而target表示的参数再也不为目标参数 // Extend jQuery itself if only one argument is passed if ( i === length ) { target = this; i--; } // 从第i个参数开始 for ( ; i < length; i++ ) { // 获取第i个参数,且该参数不为null, // 好比$.extend(target, {}, null);中的第2个参数null是不参与合并的 // Only deal with non-null/undefined values if ( (options = arguments[ i ]) != null ) { // 使用for~in获取该参数中全部的字段 // Extend the base object for ( name in options ) { src = target[ name ]; // 目标参数中name字段的值 copy = options[ name ]; // 当前参数中name字段的值 // 若参数中字段的值就是目标参数,中止赋值,进行下一个字段的赋值 // 这是为了防止无限的循环嵌套,咱们把这个称为,在下面进行比较详细的讲解 // Prevent never-ending loop if ( target === copy ) { continue; } // 若deep为true,且当前参数中name字段的值存在且为object类型或Array类型,则进行深度赋值 // Recurse if we're merging plain objects or arrays if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { // 若当前参数中name字段的值为Array类型 // 判断目标参数中name字段的值是否存在,若存在则使用原来的,不然进行初始化 if ( copyIsArray ) { copyIsArray = false; clone = src && jQuery.isArray(src) ? src : []; } else { // 若原对象存在,则直接进行使用,而不是建立 clone = src && jQuery.isPlainObject(src) ? src : {}; } // 递归处理,此处为2.2 // Never move original objects, clone them target[ name ] = jQuery.extend( deep, clone, copy ); // deep为false,则表示浅度拷贝,直接进行赋值 // 若copy是简单的类型且存在值,则直接进行赋值 // Don't bring in undefined values } else if ( copy !== undefined ) { // 若原对象存在name属性,则直接覆盖掉;若不存在,则建立新的属性 target[ name ] = copy; } } } } // 返回修改后的目标参数 // Return the modified object return target; };
在源码中进行了一下这样的判断:
// Prevent never-ending loop if ( target === copy ) { continue; }
为何要有这样的判断,咱们来看一个简单的例子,若是没有这个判断会怎么样:
var _default = {name : 'wenzi'}; var obj = {name : _default} $.extend(_default, obj); console.log(_default);
输出的_default是什么:
_default = {name : _default};
_default是object类型,里面有个字段name,值是_default,而_default是object类型,里面有个字段name,值是_default......,无限的循环下去。因而jQuery中直接不进行操做,跳过这个字段,进行下一个字段的操做。
变量值为简单类型(基元类型,如number, string, boolean)进行赋值时是不会影响上一个变量的值的,所以,若是当前字段的值为Object或Array类型,须要对其进行拆分,直到字段的值为简单类型(如number, string, boolean)时才进行赋值操做。
jQuery.extend = jQuery.fn.extend = function(){}
也就是说$.extend()
与$.fn.extend()
共用的是同一个函数体,全部的操做都是同样的,只不过两个extend使用的对象不一样罢了:$.extend()
是在jQuery($)
上进行操做的;而$.fn.extend()
是在jQuery对象上进行操做的,如$('div').extend()
.
版本1:
void function(global){ var extend, _extend, _isObject; _isObject = function(o){ return Object.prototype.toString.call(o) === '[object Object]'; } _extend = function self(destination, source){ for (var property in source) { if (source.hasOwnProperty(property)) { // 若sourc[property]是对象,则递归 if (_isObject(source[property])) { // 若destination没有property,赋值空对象 if (!destination.hasOwnProperty(property)) { destination[property] = {}; }; // 对destination[property]不是对象,赋值空对象 if (!_isObject(destination[property])) { destination[property] = {}; }; // 递归 self(destination[property], source[property]); } else { destination[property] = source[property]; }; } } } extend = function(){ var arr = arguments, result = {}, i; if (!arr.length) return {}; for (i = 0; i < arr.length; i++) { if (_isObject(arr[i])) { _extend(result, arr[i]) }; } arr[0] = result; return result; } global.extend = extend; }(window)
版本1存在的问题:咱们这里是按照参数顺序从左到右依次执行的,可是其实如果最后一个参数有的属性,前面的参数上的该属性都不须要再扩展了。其实前面的全部参数都是将本身身上有的属性而最后一个参数没有的属性补到最后一个参数上。既如此,是否是从参数列表的右侧开始扩展更好一些。
版本2:
void function(global){ var extend, _extend, _isObject; _isObject = function(o){ return Object.prototype.toString.call(o) === '[object Object]'; } _extend = function self(destination, source) { var property; for (property in destination) { if (destination.hasOwnProperty(property)) { // 若destination[property]和sourc[property]都是对象,则递归 if (_isObject(destination[property]) && _isObject(source[property])) { self(destination[property], source[property]); }; // 若sourc[property]已存在,则跳过 if (source.hasOwnProperty(property)) { continue; } else { source[property] = destination[property]; } } } } extend = function(){ var arr = arguments, result = {}, i; if (!arr.length) return {}; for (i = arr.length - 1; i >= 0; i--) { if (_isObject(arr[i])) { _extend(arr[i], result); }; } arr[0] = result; return result; } global.extend = extend; }(window)