$.extend第二篇【源码逐行分析】

458 查看

我们来填上次$.extend基础用法挖的坑,来逐行分析一下jQuery中$extend方法是怎样实现的。

首先先贴源码:


jQuery.extend = jQuery.fn.extend = function() {
    var options, name, src, copy, copyIsArray, clone,
    target = arguments[0]  {}, 
    i = 1,
    length = arguments.length,
    deep = false;

    // Handle a deep copy situation
    if ( typeof target === "boolean" ) {
        deep = target;
        target = arguments[1]  {};
        // skip the boolean and the target
        i = 2;
    }

    // Handle case when target is a string or something (possible in deep copy)
    if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
        target = {};
    }

    // extend jQuery itself if only one argument is passed
    if ( length === i ) {
        target = this;
        --i;
    }

    for ( ; i < length; i++ ) {
    // Only deal with non-null/undefined values
        if ( (options = arguments[ i ]) != null ) {
            // Extend the base object
            for ( name in options ) {
                src = target[ name ];
                copy = options[ name ];
                // Prevent never-ending loop
                if ( target === copy ) {
                    continue;
                }

                // Recurse if we're merging plain objects or arrays
                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 : {};
                    }
                    // Never move original objects, clone them
                    target[ name ] = jQuery.extend( deep, clone, copy );

                // Don't bring in undefined values
                } else if ( copy !== undefined ) {
                    target[ name ] = copy;
                }
            }
        }
    }

    // Return the modified object
     return target;
};

第一行:
jQuery.extend = jQuery.fn.extend = function() { //给jQuery对象和jQuery原型对象都添加了extend扩展方法。

源码说明,jQuery.fn等于jQuery的prototype

一:初始化变量过程:
var //每一个变量具体代表什么,我们往下分析就会得到结果。
options,
name,
src,
copy,
copyIsArray,
clone,
target = arguments[0] {}, // Return the modified object 返回的最后修改的对象
//这里初始化为第一个参数,如果没有就是空对象 (但这都是在多于一个参数且第一个参数不是boolean类型的情况,后面马上会说明)
i = 1, //这里是表示传入参数访问的下标
length = arguments.length, //参数的长度
deep = false; //是否深度复制

---------------------------------------------华丽的分割线---------------------------------------

二:初步判断参数过程:
这里是分三个简单的小判断:
(1)判断target是否是boolean,也就是判断第一个参数传入的是否布尔类型。


// Handle a deep copy situation  操作深度复制的情况
if ( typeof target === "boolean" ) {   //如果这里的target类型是boolean
    deep = target;  //这里就让深度复制的flag等于target
    target = arguments[1]  {};   //这里让target接收第二个传入的参数。
    // skip the boolean and the target
    i = 2;   //跳过布尔值和目标   (通过调整下标)
}

测试结果:


console.log( $.extend(true,{name:1}) ); //jQuery对象
/*
function( selector, context ) {
    // The jQuery object is actually just the init constructor 'enhanced'
    return new jQuery.fn.init( selector, context, rootjQuery );
}
*/
console.log( $.extend(true,{name:1},{name:2,age:18}) ); //Object {name: 2, age: 18}

问:这里为什么在传两个参数的时候,target返回的是jQuery对象呢?
答:因为这里满足了下面要说的第三个小判断,别着急,继续看下去。

(2)控制当target不是object或者function的情况。


// Handle case when target is a string or something (possible in deep copy)
if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
    target = {};  //不是object或function类型的时候,让target为空{}。
}

(3)当参数列表长度等于i的时候,扩展jQuery对象自身。


// extend jQuery itself if only one argument is passed
 if ( length === i ) {
    target = this;
    --i;  //这里--i是控制下标溢出的,因为i初始化是1,如果穿一个参数下标就会超出长度,如果第一个参数是boolean类型,这样传两个参数,同样也会溢出,具体原因可以去看第(1)个小判断。
}

测试结果:


$.extend({
    b:1,
    a:function(){
        alert(1);
    }
});
console.log($.b); //1    (注释掉--i)  undefined
$.a(); //alert(1);   (注释掉--i) Uncaught TypeError: $.a is not a function

-------------------------------------晕晕的分割线-----------------------------------------------

三:遍历参数列表给target赋值的阶段。
for ( ; i < length; i++ ) {
//这里是循环的执行体
}
在研究内部逻辑之前,首先我们先看一下for循环下i的问题:
这个i在这次的分析中,是一个还没有解决的问题,看下图:

$.extend源码i的问题

这里会多了一次循环,目前在源码里我没有找到是什么原因出现的,而且后面多遍历出了jQuery中的offsetSupport和support对象,感兴趣的同学可以深入研究一下,也欢迎大神们留评论指导(^__^)。

内部逻辑:
(1)浅层复制:

下边这段代码是深层复制的条件判定,我们先跳过,过一会再讨论,我们现在需要关注的是else if中的内容:


if ( deep && copy && ( jQuery.isPlainObject(copy)  (copyIsArray = jQuery.isArray(copy)) ) ) { 
// deep必须指定为真,copy在循环中不为undefined的情况,copy必须是new出的Object或者{}格式,或者copy是一个数组,用变量copyIsArray 来接收copy是不是数组 存储的是boolean

来看else if部分的代码(很简单的一句):


else if ( copy !== undefined ) {
    target[ name ] = copy; 
    //这里用到了name和copy两个我们还没有赋值的变量,而看这个形式obj[name] = value 你想到了什么? 
}

接下来我们来向上找,name和copy所对应的key和value在哪里:


if ( (options = arguments[ i ]) != null ) {
 //循环中arguments[i]赋值给options变量 而且要求这个值不能为null
    // Extend the base object
    for ( name in options ) {  //这里for in循环遍历每一个options
        src = target[ name ]; //src存改变之前target对象上每一个key对应的value
        copy = options[ name ]; //copy存传入对象上每一个key对应的value

这里不是很好理解,单独写一个例子便于理解:
比如我们这样调用:$.extend({},{name:1},{name:2,age:18});
参数列表中的每一项,分别是{} 、{name:1} 、{name:2,age:18}
这个时候由于i初始化是1 所以options是访问不到{},而同时的状态下target指向的是{}。
下标0的元素不会访问到的demo:


function rep(){
    var name,
    options,
    copy,
    len = arguments.length,
    i = 1;
    for(; i<len; i++){
        if(  (options = arguments[i]) != null ){
            alert(1);
        }
    }
}

rep({}); //这个时候不会alert 
rep({},{}); //这时才会 

一个简单抽离出来的浅层复制代码实现:


function rep(){
    var name,
        options,
        copy,
        len = arguments.length,
        i = 1,
        target = arguments[0],
        src,
        copy;
    for(; i<len; i++){
        if(  (options = arguments[i]) != null ){
            for(name in options){
                src = target[name];
                copy = options[name];

                console.log(src); //undefined  undefined  18 undefined
                console.log(copy);//1   18  '16'  Object {eat: "eat", run: "run"}
                //这个结果就很明显的说明了 name、src、copy都指的是什么了。

                //最后扩展的过程就是把copy上获取到的不为undefined的值,复制到            target的同一个key上。(同key会覆盖)
                if(copy !== undefined){
                    target[name] = copy;
                }
            }
        }
    }
    return target;
}

var res = rep({},{name:1,age:18},{age:'16',params:{eat:'eat',run:'run'}});
console.log(res); //Object {name: 1, age: "16", params: Object} 也就意味着把第二个之后的参数对象内容都扩展到第一个空{}上了。

(2)深层复制:
第一句判断,前边注释过,就不多说了,直接看内部的逻辑:


// Recurse if we're merging plain objects or arrays
if ( deep && copy && ( jQuery.isPlainObject(copy)  (copyIsArray = jQuery.isArray(copy)) ) ) {
    if ( copyIsArray ) { //这里是copy是数组的情况
        copyIsArray = false; //先从新指定为false,要不然下次循环判定就会出问题
        clone = src && jQuery.isArray(src) ? src : []; 
        //如果src存在且是数组的话就让clone副本等于src否则等于空数组。
    } else { //copy是对象的情况
        clone = src && jQuery.isPlainObject(src) ? src : {};
        //同数组的操作
    }

    // Never move original objects, clone them
    target[ name ] = jQuery.extend( deep, clone, copy );
    //这里用递归的形式在走一次entend方法,来判断经过一次复制之后,是否还存在数组或对象形式的copy,如果有则再次走这个if分支,没有则走上面浅层复制的分支。
} 

测试代码:


//这里在fon in 循环中加了两条console用来帮助我们查看结果。
console.log('src:'+JSON.stringify(src));
console.log('copy:'+JSON.stringify(copy));

//这里是调用的测试代码部分:
var res = $.extend(
    true,
    {
        params:{
            add:'no'
        }
    },
    {
        name:1,
        params:{
            add:'add',
            remove:'remove'
        }
    }
);
console.log(res);

测试结果截图:

深层复制测试结果02

数组的格式同理,这里就不再进行测试了,到这里$.extend源码的解析基本就结束了。

最后来总结一下我们刚开始抛出的每一个变量含义的问题:

$extend初始化变量含义

下次会更新$.extend的最终章,偏向于应用,会简单的实现一个$.extend扩展组件,敬请期待,( ^_^ )/~~拜拜喽!