jQuery源码extend方法

11 篇文章 0 订阅

看源码之间得先知道几个概念,不然只能像我一样盯着屏幕发呆…

浅拷贝

上面的博文写的非常详细了
这里总结一下

Arrary.prototypeObject
concat sliceassign

都是浅拷贝,也就是如果出现数组嵌套,对象嵌套的时候,存放的都是原来数组对象中的引用地址,而不是新开辟的堆内存。也就意味着如果有对象嵌套的情况,修改拷贝的对象也会影响原来被拷贝的对象。

深拷贝

深拷贝跟浅拷贝不同,修改拷贝的对象对原来没有影响

突然想起之前一个例子能很好说明这个问题

//=> 浅拷贝
var obj1 = {};
var obj2 = obj1;
obj1 === obj2 //=> true

//=> 深拷贝
{} === {} // => false

extend方法

jQuery.extend([deep], target, object1, [objectN])

用一个或多个其他对象来扩展一个对象,返回被扩展的对象。
[deep]: true为深拷贝,不传默认浅拷贝

var obj1 = {
   a: 1,
   b: {b1: 1, b2: 2}
};

var obj2 = {
    b: {b1: 3, b3: 4},
    c: 3
};

var obj3 = {
    d: 4
};
//=> 浅拷贝
console.log($.extend(obj1, obj2, obj3));
// {
//    a: 1,
//    b: { b1: 3, b3: 4 },
//    c: 3,
//    d: 4
// }

//=> 深拷贝
console.log($.extend(true, obj1, obj2, obj3));
// {
//    a: 1,
//    b: { b1: 3, b2: 2, b3: 4 },
//    c: 3,
//    d: 4
// }

源码实现

先考虑浅拷贝,不进行深层次的覆盖

var obj1 = {
   a: 1,
   b: {b1: 1, b2: 2}
};

var obj2 = {
    b: {b1: 3, b3: 4},
    c: 3
};

var obj3 = {
    d: 4
};
console.log(extend(obj1, obj2, obj3));
// {
//    a: 1,
//    b: { b1: 3, b3: 4 },
//    c: 3,
//    d: 4
// }

尝试的第一个版本

function extend() {
        var target = arguments[0],
            options,
            copy,
            item, // 被拷贝对象的属性名
            i = 1;

        if (typeof target !== 'object') {
            target = {};
        }

        for (; i < arguments.length; i++) {
            // 被拷贝对象options
            options = arguments[i];

            for (item in options) {
                // 拷贝的属性值
                copy = options[item];

                if (copy !== null) {
                    target[item] = copy;
                }

            }
        }
        return target;
    }

考虑的还是太少了
if (options != null) 那主要是排除nullundefined的情况。 因为null == undefined //=> true

function extend() {
        var target = arguments[0],
            options,
            copy,
            item,
            i = 1;

        if (typeof target !== 'object') {
            target = {};
        }

        for (; i < arguments.length; i++) {
            options = arguments[i];

            // 排除传入null, 不传(undefined)的情况
            if (options != null) {
                for (item in options) {
                    copy = options[item];

                    // 这里应该是undefined, 属性值即使为null也应该覆盖
                    if (copy !== undefined) {
                        target[item] = copy;
                    }

                }
            }
        }
        return target;
    }

extend实现深拷贝

功力太差 只能勉强拼凑出功能

function extend() {
        var target = arguments[0],
            options,
            copy,
            src,
            item,
            i = 1,
            deep = false; // 默认为浅拷贝

        if (typeof target === 'boolean') {
            //=> 全部往后移一位
            target = arguments[1];
            i++;
        } else if (typeof target !== 'object') {
            target = {};
        }

        deep = arguments[0] === true ? true : false;

        for (; i < arguments.length; i++) {
            options = arguments[i];

            // 排除传入null, 不传(undefined)的情况
            if (options != null) {
                for (item in options) {
                    copy = options[item];
                    src = target[item];

                    if (deep && typeof copy === 'object') {
                    	// 递归调用
                        target[item] = extend(deep, src, copy);
                        // 深拷贝赋值完跳出本轮循环!!!否则后面的浅拷贝会覆盖得到的结果
                        continue;
                    }
					
					// 属性赋值
                    if (copy !== undefined) {
                        target[item] = copy;
                    }

                }
            }
        }
        return target;
    }

之前一直不明白extend深拷贝是如何切断跟原来嵌套对象的联系的。

var obj1 = {
        a: 1
    };
var obj2 = {
    a: {a : 1}
};
$.extend(true, obj1, obj2);
obj1.a.a = 2;
console.log(obj1.a.a); //=> 原对象不改变
//=> 浅拷贝
obj1.a = obj2.a

//=> 深拷贝,此时obj2.a为对象,再次调用extend(true,1,{a:1})
// 这个时候target为1, target不是对象,重新赋值
 target = {} //=> 就是在这里开辟了新的内存空间
 //=> 为新的内存空间赋值
 target.a = obj2.a;
 return target;

自己动手写到这才算理解extend深浅拷贝实现原理。
浅拷贝直接赋值。
深拷贝,如果被拷贝值是对象,就再次递归调用,直到被拷贝值是基本数据类型

当然了,上面写的太粗糙了,但是足矣明白原理。

第三版

  1. 第一个参数为false,undefinednull等统一提供一个新的空对象
  2. 第一个参数有值但不是对象类型,也提供一个新的空对象
  3. 第一个参数为true,进行深拷贝,第二个参数为target
    第一个参数为true的情况下会修改target,所以应在这些判断完成以后再对target进行是否对象判断。
function extend() {
        var options,
            target,
            copy,
            src,
            item,
            i = 1,
            deep = false; // 默认为浅拷贝

        // 考虑了第一个参数为 null, undefined(不传参数)以及false的情况
        target = arguments[0] || {};

        // 上面考虑了false,这里只剩下true
        if (typeof target === 'boolean') {
           target = arguments[i] || {};
           i++;
           deep = true;
        }

        // target不为object是无法进行复制的,比如'1'
        if (typeof target !== 'object') {
            target = {};
        }

        for (; i < arguments.length; i++) {
            options = arguments[i];

            // 排除传入null, 不传(undefined)的情况
            if (options != null) {
                for (item in options) {
                    copy = options[item];
                    src = target[item];

                    if (deep && copy && typeof copy === 'object') {
                        target[item] = extend(deep, src, copy);
                        continue;
                    }


                    // 这里应该是undefined 属性值即使为null也应该覆盖
                    if (copy !== undefined) {
                        target[item] = copy;
                    }

                }
            }
        }
        return target;
    }

函数对象也是对象

if (typeof target !== 'object' || !jQuery.isFunction(target)) {
   target = {};
}

类型不一致

var obj1 = {
    a: 1,
    b: {
        c: 2
    }
}

var obj2 = {
    b: {
        c: [5],

    }
}

var d = extend(true, obj1, obj2)
console.log(d);

我们预期会返回这样一个对象:

{
    a: 1,
    b: {
        c: [5]
    }
}

然而返回了这样一个对象:

{
    a: 1,
    b: {
        c: {
            0: 5
        }
    }
}

首先我们在函数的开始写一个 console 函数比如:console.log(1),然后以上面这个 demo 为例,执行一下,我们会发现 1 打印了三次,这就是说 extend 函数执行了三遍,让我们捋一捋这三遍传入的参数:

第一遍执行到递归调用时:

var src = { c: 2 };
var copy = { c: [5]};

target[name] = extend(true, src, copy);

第二遍执行到递归调用时:

var src = 2;
var copy = [5];
target[name] = extend(true, src, copy);

第三遍进行最终的赋值,因为 src 是一个基本类型,我们默认使用一个空对象作为目标值,所以最终的结果就变成了对象的属性!

为了解决这个问题,我们需要对目标属性值和待复制对象的属性值进行判断:

判断目标属性值跟要复制的对象的属性值类型是否一致:

  • 如果待复制对象属性值类型为数组,目标属性值类型不为数组的话,目标属性值就设为 []

  • 如果待复制对象属性值类型为对象,目标属性值类型不为对象的话,目标属性值就设为 {}

注意:
这种多层嵌套的对象就直接赋值了,$.extend方法不再一直递归下去。

copy = {b:{b:{b:1}}};

// 判断是否扁平对象或者数组才进入递归
 if (deep && copy && (isPlainObject(copy) || (copyIsArray
 = Array.isArray(copy)))) {

     if (copyIsArray) {
         copyIsArray = false;
         src = Array.isArray(src) ? src : [];
     } else {
         src = isPlainObject(src) ? src : {};
     }

     target[item] = extend(deep, src, copy);
     continue;
 } else if (copy !== undefined) {
     target[item] = copy;
 }

jQuery@3.5.1

// Ensure proper type for the source value
if ( copyIsArray && !Array.isArray( src ) ) {
	clone = [];
} else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) {
	clone = {};
} else {
	clone = src;
}
copyIsArray = false;

逻辑也是一样的
copy是数组就查看src是不是数组,不是就赋值[]
copy是扁平对象就查看src是不是扁平对象,不是就赋值{}
以上都符合的话保持原样,总之以copy进行主导来赋值,让src的类型与copy相匹配才能进入递归

能明白这点,底下博客里的题目就没什么难度了!

循环引用

实际上,我们还可能遇到一个循环引用的问题,举个例子:

var a = {name : b};
var b = {name : a}
var c = extend(a, b);

互相引用,传进去的永远是对象

if (copy === target) {
	continue;
}

防止原型污染

jQuery@3.5.1还添加了防止原型污染的处理

if ( name === "__proto__" || target === copy ) {
	continue;
}

攻击者可以传入这样的操作来修改源代码,导致一些安全问题。

var myObject ={ “myProperty” : “a”, "__proto__" : { "isAdmin" : true } }var newObject = jQuery.extend(true, {}, JSON.parse(myObject))

// if you do console.log({}.isAdmin) you’ll get true returned
// later down the application source code we may try to detect if the user is an admin or not
If (user.isAdmin === true) {
  // do something like load the relevant interface, run an Ajax call, access localStorage, etc
}

参考链接

最终版本

function extend() {
        var options,
            target,
            copy,
            copyIsArray,
            src,
            item,
            i = 1,
            deep = false; // 默认为浅拷贝

        // 考虑了第一个参数为 null, undefined(不传参数)以及false的情况
        target = arguments[0] || {};

        // 上面考虑了false,这里只剩下true
        if (typeof target === 'boolean') {
           target = arguments[i] || {};
           i++;
           deep = true;
        }

		// 普通对象或函数对象都可以合并
        if (typeof target !== 'object' || !jQuery.isFunction(target)) {
            target = {};
        }

        for (; i < arguments.length; i++) {
            options = arguments[i];

            // 排除传入null, 不传(undefined)的情况
            if (options != null) {

				// 遍历被拷贝对象的每个属性值
                for (item in options) {
                    copy = options[item];
                    src = target[item];

                    if (target === copy || item === '__proto__') {
                        continue;
                    }

                    // 判断是否扁平对象或者数组
                    if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray
                    = Array.isArray(copy)))) {

						// 类型匹配
                        if (copyIsArray) {
                            copyIsArray = false;
                            src = Array.isArray(src) ? src : [];
                        } else {
                            src = jQuery.isPlainObject(src) ? src : {};
                        }

                        target[item] = extend(deep, src, copy);
                        continue;

                    } else if (copy !== undefined) {
                        target[item] = copy;
                    }

                }
            }
        }
        return target;
    }

总算看明白了,还有遗留下了类型判断需要学习!
😭😭😭😭
重要参考
这个作者写的博客实在是太好了!

总结

很多逻辑以及处理真的值得反复阅读。
让我印象特别深刻的是这几行

// false,'',null,undefined,NaN => {}
 target = arguments[0] || {};

 // 上面包含了false,这里只剩下true
 if (typeof target === 'boolean') {
    target = arguments[i] || {};
    i++;
    deep = true;
 }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值