从jQuery.extend()中学习对象的扩展和深遍历

由于本人水平有限,若有误导嫌疑,请不吝赐教!

$.extend是jQuery中极为重要的一个方法,它是jQ内置的第一个方法,其他所有方法都在它的基础上进行扩展的,所以非常有必要学习这个API的实现。

用法非常简单,可以参照菜鸟教程的示例:菜鸟教程 $.extend 示例

$.extend( [deep ], target, object1 [, objectN ] )
  1. deep: 是否深度合并
  2. target: 需要合并的源对象
  3. 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无影响。

这是因为基础类型的赋值,会在对象变量上创建一个新值,并把该值放到新变量的位置上。如下图所示:

从jQuery.extend()中学习对象的扩展和深遍历_第1张图片
复制前
从jQuery.extend()中学习对象的扩展和深遍历_第2张图片
复制后

2.引用类型的复制
引用类型包括: Object、Array

var obj1 = new Object()
obj1.name = '电光毒龙';
var obj2 = obj1;
obj2.name = '钻';
console.log(obj1.name)  // '钻'
修改obj2后,也改变了obj1

说明obj1和obj2引用的是同一个对象,我们可以通过obj2改变obj1。所以引用类型的复制不能通过简单的赋值实现。变量和堆栈中的对象关系如下图:
从jQuery.extend()中学习对象的扩展和深遍历_第3张图片

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;
};


三、分析

有以下几处值得品味

  1. 防止基础类型出现
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

你可能感兴趣的:(从jQuery.extend()中学习对象的扩展和深遍历)