由于本人水平有限,若有误导嫌疑,请不吝赐教!
$.extend是jQuery中极为重要的一个方法,它是jQ内置的第一个方法,其他所有方法都在它的基础上进行扩展的,所以非常有必要学习这个API的实现。
用法非常简单,可以参照菜鸟教程的示例:菜鸟教程 $.extend 示例
$.extend( [deep ], target, object1 [, objectN ] )
- deep: 是否深度合并
- target: 需要合并的源对象
- object: 需要合并的目标对象
注意,合并后,target会被修改!
一、知识准备(参考《javascript高级程序设计》第三版)
1.基础类型的复制
基础类型包括:String、Number、Boolean、undefined、null。它们的复制在代码上体现为赋值:
var num1 = 5;
var num2 = num1;
num2 = 10;
console.log(num1) //5
console.log(num2) //10
修改num2后,对num1无影响。
这是因为基础类型的赋值,会在对象变量上创建一个新值,并把该值放到新变量的位置上。如下图所示:
2.引用类型的复制
引用类型包括: Object、Array
var obj1 = new Object()
obj1.name = '电光毒龙';
var obj2 = obj1;
obj2.name = '钻';
console.log(obj1.name) // '钻'
修改obj2后,也改变了obj1
说明obj1和obj2引用的是同一个对象,我们可以通过obj2改变obj1。所以引用类型的复制不能通过简单的赋值实现。变量和堆栈中的对象关系如下图:3.判断数据类型:
可以参照我的上一篇文章:从jQuery.type()中学习如何判断数据类型
jQ中还用到了 isPlainObject() 判断对象,文章结尾会进行介绍和说明
二、核心代码
根据上述,对象的合并,实际上是对象(引用类型)深遍历时进行基础数据赋值的过程。我们将第一个传入的对象称为 源对象,之后传入的统称为目标对象
jQuery.extend = jQuery.fn.extend = function() {
var options, //目标对象,指向参数
name, //目标对象中的属性(键)
src, //源对象的属性值(值)
copy, //目标对象的属性值
copyIsArray, //判断属性值是否为数组(布尔值)
clone, //源对象的属性值(值)
target = arguments[ 0 ] || {},//源对象
i = 1,
length = arguments.length,//参数数量
deep = false; //是否深度合并,默认false
// 处理深拷贝
if ( typeof target === "boolean" ) {
deep = target;
//源对象指向第二个参数, 遍历时跳过第一个参数
target = arguments[ i ] || {};
i++;
}
// Handle case when target is a string or something (possible in deep copy)
// 当源对象为字符串或其他非基础数据类型时执行(此时可能在深拷贝过程中)
if ( typeof target !== "object" && !jQuery.isFunction( target ) ) {
target = {};
}
// 如果只传递一个参数,则扩展到jQuery本身。此时jQuery为源对象
if ( i === length ) {
target = this;
i--;
}
for ( ; i < length; i++ ) {
// 程序不处理 null或undefined 参数
if ( ( options = arguments[ i ] ) != null ) {
// 遍历目标对象
for ( name in options ) {
src = target[ name ];
copy = options[ name ];
// 防止无限循环
if ( target === copy ) {
continue;
}
// 目标对象的属性值若为 Object 或 Array ,进行递归
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 );
// 若为基础数据类型且不为undefined,赋值即可
} else if ( copy !== undefined ) {
target[ name ] = copy;
}
}
}
}
// 返回修改后的源对象,若不想修改传入的对象,可以把 {} 作为源对象传入
return target;
};
三、分析
有以下几处值得品味
- 防止基础类型出现
if ( typeof target !== "object" && !jQuery.isFunction( target ) ) {
target = {};
}
源码中注释说到,这里是为了深拷贝中重置string or something。但是 target 只可能是$.extend([deep ], target, object1 [, objectN ]) 中传入的target,或者clone,而 clone必定是 [] 或 {} ,所以不可能出现注释中所说的情况,还请 读者们能够赐教
2.防止无限循环
if ( target === copy ) {
continue;
}
这是为了防止以下情况出现
var obj1 = {name: '电光毒龙'};
var obj2 = {name: obj1};
四、jQuery.isPlainObject和jQuery.isArray分析
从字面意思可以看出,这是判断数据是否为对象和是否为数组
1.jQuery.isArray
jQuery.extend( {
isArray: Array.isArray
})
这段代码在 jQuery-3.1.1 第282行,其方法直接调用原生数组的isArray方法,此方法是ECMA5.1版发布的标准,不支持IE8及以下低版本浏览器。
2.jQuery.isPlainObject
isPlainObject: function( obj ) {
var proto, Ctor;
// Detect obvious negatives
// Use toString instead of jQuery.type to catch host objects
if ( !obj || toString.call( obj ) !== "[object Object]" ) {
return false;
}
proto = getProto( obj );
// Objects with no prototype (e.g., `Object.create( null )`) are plain
if ( !proto ) {
return true;
}
// 判断传入对象的原型链是否存在"constructor" ,若有则赋值给Ctor
Ctor = hasOwn.call( proto, "constructor" ) && proto.constructor;
// 判断传入对象的构造函数是否为 Object
return typeof Ctor === "function" && fnToString.call( Ctor ) === ObjectFunctionString;
}
这段代码在 jQuery-3.1.1 第302行。其中:
toString === Object.prototype.toString
getProto === Object.getPrototypeOf
hasOwn === Object.prototype.hasOwnProperty
fnToString === Object.prototype.hasOwnProperty.toString
ObjectFunctionString === "function Object() { [native code] }"
此方法是用于判断传入对象是否继承原生Object,因为实例化函数构造器所得的对象(在JAVA中如同class的概念)不应该被当做普通对象进行扩展。eg:
//构造函数与普通函数没有任何区别,仅仅是调用方法不同,它就起到不同的作用。约定上我们用首字母大写表示此函数为构造函数
function Human() {};
var human = new Human();
$.isPlainObject(human ) //返回false