看源码之间得先知道几个概念,不然只能像我一样盯着屏幕发呆…
浅拷贝
上面的博文写的非常详细了
这里总结一下
Arrary.prototype | Object |
---|---|
concat slice | assign |
都是浅拷贝,也就是如果出现数组嵌套,对象嵌套的时候,存放的都是原来数组对象中的引用地址,而不是新开辟的堆内存。也就意味着如果有对象嵌套的情况,修改拷贝的对象也会影响原来被拷贝的对象。
深拷贝
深拷贝跟浅拷贝不同,修改拷贝的对象对原来没有影响
突然想起之前一个例子能很好说明这个问题
//=> 浅拷贝
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) 那主要是排除null
和undefined
的情况。 因为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深浅拷贝实现原理。
浅拷贝直接赋值。
深拷贝,如果被拷贝值是对象,就再次递归调用,直到被拷贝值是基本数据类型
当然了,上面写的太粗糙了,但是足矣明白原理。
第三版
- 第一个参数为
false
,undefined
,null
等统一提供一个新的空对象 - 第一个参数有值但不是对象类型,也提供一个新的空对象
- 第一个参数为
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;
}