理解jquery的深拷贝

好像自从使用框架之后,对jquery的依赖越来越低了,其好像已经慢慢作为一个工具库的存在了。新项目商量之下,为了减小文件大小,干脆直接不用jquer2,对于一些需要的工具函数直接从jquery提取到一个自己写的工具文件tool.js中。在提取的过程中,也慢慢理解了jquery一些工具函数的源码

深拷贝和浅拷贝的使用场景不同,并没有好坏之分,像对一些基本数据类型,直接可以使用浅拷贝对处理数据。但是对于基本引用类型如嵌套对象,数组(包含着对象的数组),那么就需要使用到深拷贝了。

不想看前面深浅拷贝对比的,可以直接拉到第二章看jquery源码实现


1.浅拷贝解析

原生js也有一些提供拷贝的函数,比如数组的Array.slice(0),Array.concat(),对象的Object.create(),Object.assign()等等,但是都是浅拷贝,遇到二维数组,嵌套对象就通通失败了(以前不懂的时候,真的被坑的不要不要的啊)。

比如下面这个例子,都是在只有基本数据类型的情况下,使用浅拷贝就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let arr = [1,2,34,5,67,8,9];
let cloneArr = arr.slice(4);
console.log(cloneArr); // [67, 8, 9]
cloneArr[0] = 100; // 修改cloneArr
console.log(arr); // [1,2,34,5,67,8,9],修改cloneArr不影响原数组arr
----------------
let obj = {a : 1,b : 2,c : 3,};
let cloneObj = Object.assign({},obj);// 将拷贝的属性值拷贝到目标对象,然后返回目标对象
console.log(cloneObj); // cloneObj = {a : 1,b : 2,c : 3,};
cloneObj.a = 444; //修改对象
console.log(obj); // obj = {a : 1,b : 2,c : 3,}; 修改拷贝对象不影响源对象

但是如果以上例子将基本数据类型换成引用类型Object和Array呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let arr = [1,2,{a : 3},{b : 4},5];
let cloneArr = arr.slice(2);
console.log(cloneArr); // [{a : 3},{b : 4},5];
cloneArr[0].a = 100; // 修改cloneArr
console.log(arr); // [1,2,{a : 100},{b : 4},5],修改cloneArr影响原数组arr
----------------
let obj = {a : {aa : 1},b : {bb : 2},c : 3,};
let cloneObj = Object.assign({},obj);// 将拷贝的属性值拷贝到目标对象,然后返回目标对象
console.log(cloneObj); // cloneObj = {a : {aa : 1},b : {bb : 2},c : 3,};
cloneObj.a.aa = 100; //修改对象
console.log(obj); // obj = {a : {aa : 100},b : {bb : 2},c : 3,}; 修改拷贝对象影响到了源对象

为什么会这样子,原因其实也不复杂。js内存分为栈内存和堆内存。所有的基本数据类型都是存储在栈内存中,而引用类型则是存储在堆内存中,提供了一个地址放在了栈内存中。当我们要获取引用类型的值时,先从栈内存获得地址,再根据地址去堆内存中获得值。因此也叫按引用访问。

(去网上浅拷贝了一张图片,因为拷贝了一个图片地址)
去网上浅拷贝了一张图片,拷贝了一个图片地址

而我们上面例子中,每个数组和对象每个属性存储的引用类型obj其实是个地址,我们只是简单的拷贝了属性值,其实就是拷贝了一个地址。所以我们在新对象里进行修改时,由于是通过同一个地址修改了值。因为和原对象共用了一个地址,所以自然就修改了原对象的值了。

2.深拷贝解析

前面解析了浅拷贝。因为我们项目对大型数据处理占据了大头,其中不可避免的会经常用到深拷贝这块。那么深拷贝是怎么实现的。

其实也很简单,就是根据地址找到你堆内存中的值,不断递归深入拷贝下去,直到为基本数据类型为止,接下去就贴上深拷贝代码。

在讲jquey前,还有一个很暴力的方式JSON.parse()和JSON.stringify();缺点是

  • 数据不能包含函数。
  • 如果某个对象属性值为null,会形成一个对象的闭环
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    let obj = {
    a: 1,
    b: 2,
    c: [1,2,3],
    d: function() {
    console.log("asdfghj");
    }
    };
    let result1 = JSON.stringify(target);
    console.log(result1); // 输出结果为"{"a":1,"b":2,"c":[1,2,3]}",函数直接没了
    const obj = {
    foo: {
    name: 'foo',
    bar: {
    name: 'bar'
    baz: {
    name: 'baz',
    aChild: null // 待会将指向obj.bar
    }
    }
    }
    }
    obj.foo.bar.baz.aChild = obj.foo // foo->bar->baz->aChild->foo形成环
    JSON.stringify(obj) // => TypeError: Converting circular personucture to JSON

好了,最后贴上jquery深拷贝的代码和自己一些理解的注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
$.fn.extend = function () {
//jquery喜欢在初始定义好所有的变量
let options,// 被拷贝的对象
name,// 遍历时的属性
src,// 返回对象本身的属性值
copy,// 需要拷贝的内容
copyIsArray,// 判断是否为数组
clone,// 返回拷贝的内容
target = arguments[0] || {},
i = 1,
length = arguments.length,
deep = false;// 默认为浅拷贝
//target 是传入的第一个参数,表示是否要深递归
if(typeof target === 'boolean'){
deep = target;
//既然为boolean,则此处初始化target为第二个参数或者空对象
target = arguments[i] || {};
// 如果传了类型为 boolean 的第一个参数,i 则从 2 开始
i ++
}
//如果传入的第一个参数不是对象或者其他,初始化为一个空对象
if(typeof target !== 'object' && $.isFunction(target)){
target = {};
}
//如果只传入了一个参数,表示是jquery静态方法,直接返回自身
if(i === length){
target = this;
i --;
}
for(; i < length; i ++){
if((options = arguments[i]) !== null ){
for( name in options){
src = target[name];//获得源对象的值
copy = options[name];//获得要拷贝对象的值
//说是为了避免无限循环,例如 extend(true, target, {'target':target});
if(target === copy) continue;
//如果是数据正确,且是一个纯粹的对象(纯粹的对象指的是 通过 "{}" 或者 "new Object" 创建的)或者是一个数组的话
if(deep && copy && ($.isPlainObject(copy) || (copyIsArray = Array.isArray(copy)))){
//如果是一个数组的话
if(copyIsArray){
copyIsArray = false;
clone = src && Array.isArray(src) ? src : [];//判断源对象是不是数组,如果不是,直接变为空数组,拷贝属性高优先原则
} else {
clone = src && $.isPlainObject(src) ? src : {};//判断原对象属性是否有值,如果有的话,直接返回原值,否则新建一个空对象
}
//继续深拷贝下去
target[name] = $.extend(deep,clone,copy);
}else if(copy !== undefined){
//如果不为空,则不是需要深拷贝的数据和对象,而是string,data,boolean等等,可以直接赋值
target[name] = copy;
}
}
}
}
// 返回新的拷贝完的对象
return target;
}

在看上段代码中,又发现了几个好方法在业务中会用到的,可以让代码更严谨

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//判断数据类型
//判断是否为纯正的数据对象
isPlainObject: function( obj ) {
//如果数据不正确,不是对象类型,或者是dom对象,window对象,则直接返回false
if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) {
return false;
}
//这段代码是为了兼容IE89存在的,查看是否有constructor属性,如果没有必然是数据对象
try {
if ( obj.constructor &&
!core_hasOwn.call(obj, "constructor") &&
!core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) {
return false;
}
} catch ( e ) {
return false;
}
//对象中key的顺序会将自身属性排在最前面遍历,如果最后一个还是自身属性,则必然所有属性都是自己的
var key;
for ( key in obj ) {}
return key === undefined || core_hasOwn.call( obj, key );
},

以上基本就是jquery.extend代码的解析了。extend是jquery中一个极其重要的方法,jquery本身就用它扩展了许多静态方法和实例方法