javascript中的深拷贝和浅拷贝

javaScript 的变量类型

基本类型

基本数据类型有 numberstringbooleannullundefinedsymbol 以及未来 ES10 新增的 BigInt(任意精度整数)七类,变量是直接按值存放的,存放在栈内存中的简单数据段,可以直接访问。

引用类型

存放在堆内存中的对象,变量保存的是一个指针,这个指针指向另一个位置。当需要访问引用类型(如对象,数组等)的值时,首先从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据。

JavaScript 存储对象都是存地址的,所以浅拷贝会导致 obj1 和 obj2 指向同一块内存地址。改变了其中一方的内容,都是在原来的内存上做修改会导致拷贝对象和源对象都发生改变,而深拷贝是开辟一块新的内存地址,将原对象的各个属性逐个复制进去。对拷贝对象和源对象各自的操作互不影响。

例如:数组拷贝

1
2
3
4
5
6
7
// 浅拷贝,双向改变,指向同一片内存空间
var arr1 = [1, 2, 3];
var arr2 = arr1;
arr1[0] = "change";

console.log("shallow copy: " + arr1 + " "); // shallow copy: change,2,3
console.log("shallow copy: " + arr2 + " "); // shallow copy: change,2,3

浅拷贝和深拷贝的区分

深复制和浅复制只针对像 Object, Array 这样的复杂对象的

浅复制只复制一层对象的属性,而深复制则递归复制了所有层级

一个简单的浅复制实现:

1
2
3
4
5
6
7
8
9
10
11
12
var obj = { a: 1, arr: [2, 3] };
var shallowObj = shallowCopy(obj);

function shallowCopy(src) {
var dst = {};
for (var prop in src) {
if (src.hasOwnProperty(prop)) {
dst[prop] = src[prop];
}
}
return dst;
}

结果:

1
2
shallowObj.arr[1] = 5;
obj.arr[1]; // = 5

深复制则不同,它不仅将原对象的各个属性逐个复制出去,而且将原对象各个属性所包含的对象也依次采用深复制的方法递归复制到新对象上。这就不会存在上面 obj 和 shallowObj 的 arr 属性指向同一个对象的问题。

需要注意的是,如果对象比较大,层级也比较多,深复制会带来性能上的问题。在遇到需要采用深复制的场景时,可以考虑有没有其他替代的方案。在实际的应用场景中,也是浅复制更为常用。

浅拷贝的实现

简单的引用复制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function shallowClone(copyObj) {
var obj = {};
for (var i in copyObj) {
obj[i] = copyObj[i];
}
return obj;
}
var x = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var y = shallowClone(x);
console.log(y.b.f === x.b.f); // true

Object.assign()

Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。

1
2
3
4
5
6
7
var x = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var y = Object.assign({}, x);
console.log(y.b.f === x.b.f); // true

深拷贝的实现

实现深复制的的基本思路

  1. 检测当前属性是否为对象
  2. 因为数组是特殊的对象,所以,在属性为对象的前提下还需要检测它是否为数组。
  3. 如果是数组,则创建一个[]空数组,否则,创建一个{}空对象,并赋值给子对象的当前属性。然后,递归调用 extendDeep 函数。

递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 使用递归的方式实现数组、对象的深拷贝
function deepClone1(obj) {
//判断拷贝的要进行深拷贝的是数组还是对象,是数组的话进行数组拷贝,对象的话进行对象拷贝
var objClone = Array.isArray(obj) ? [] : {};
//进行深拷贝的不能为空,并且是对象或者是
if (obj && typeof obj === "object") {
for (key in obj) {
if (obj.hasOwnProperty(key)) {
if (obj[key] && typeof obj[key] === "object") {
objClone[key] = deepClone1(obj[key]);
} else {
objClone[key] = obj[key];
}
}
}
}
return objClone;
}

用 for..in 遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// _tmp和result是相互独立的,没有任何联系,有各自的存储空间。
let deepClone = function (obj) {
let _tmp = JSON.stringify(obj); // 将对象转换为json字符串形式
let result = JSON.parse(_tmp); // 将转换而来的字符串转换为原生js对象
return result;
};

let obj1 = {
weiqiujaun: {
age: 20,
class: 1502
},
liuxiaotian: {
age: 21,
class: 1501
}
};

let test = deepClone(obj1);
console.log(test);

Array.prototype.forEach

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let deepClone = function (obj) {
let copy = Object.create(Object.getPrototypeOf(obj));
let propNames = Object.getOwnPropertyNames(obj);
propNames.forEach(function (items) {
let item = Object.getOwnPropertyDescriptor(obj, items);
Object.defineProperty(copy, items, item);
});
return copy;
};

let testObj = {
name: "weiqiujuan",
sex: "girl",
age: 22,
favorite: "play",
family: { brother: "wei", mother: "haha", father: "heihei" }
};
let testRes2 = deepClone(testObj);
console.log(testRes2);

Array 的 slice 和 concat 方法

Array 的 slice 和 concat 方法不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。之所以把它放在深拷贝里,是因为它看起来像是深拷贝。而实际上它是浅拷贝。原数组的元素会按照下述规则拷贝:

  • 如果该元素是个对象引用 (不是实际的对象),slice 会拷贝这个对象引用到新的数组里。两个对象引用都引用了同一个对象。如果被引用的对象发生改变,则新的和原来的数组中的这个元素也会发生改变。
  • 对于字符串、数字及布尔值来说(不是 StringNumber 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。在别的数组里修改这些字符串或数字或是布尔值,将不会影响另一个数组。

如果向两个数组任一中添加了新元素,则另一个不会受到影响。例子如下:

1
2
3
4
5
6
7
var array = [1, 2, 3];
var array_shallow = array;
var array_concat = array.concat();
var array_slice = array.slice(0);
console.log(array === array_shallow); //true
console.log(array === array_slice); //false,"看起来"像深拷贝
console.log(array === array_concat); //false,"看起来"像深拷贝

可以看出,concat 和 slice 返回的不同的数组实例,这与直接的引用复制是不同的。而从另一个例子可以看出 Array 的 concat 和 slice 并不是真正的深复制,数组中的对象元素(Object,Array 等)只是复制了引用。如下:

1
2
3
4
5
6
7
8
9
10
11
var array = [1, [1, 2, 3], { name: "array" }];
var array_concat = array.concat();
var array_slice = array.slice(0);

array_concat[1][0] = 5; //改变array_concat中数组元素的值
console.log(array[1]); //[5,2,3]
console.log(array_slice[1]); //[5,2,3]

array_slice[2].name = "array_slice"; //改变array_slice中对象元素的值
console.log(array[2].name); //array_slice
console.log(array_concat[2].name); //array_slice

JSON 对象的 parse 和 stringify

使用较为简单,可以满足基本的深拷贝需求,而且能够处理 JSON 格式能表示的所有数据类型

JSON 对象是 ES5 中引入的新的类型(支持的浏览器为 IE8+),JSON 对象 parse 方法可以将 JSON 字符串反序列化成 JS 对象,stringify 方法可以将 JS 对象序列化成 JSON 字符串,借助这两个方法,也可以实现对象的深拷贝。

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
//例1
var source = { name: "source", child: { name: "child" } };
var target = JSON.parse(JSON.stringify(source));

target.name = "target"; //改变target的name属性
console.log(source.name); //source
console.log(target.name); //target

target.child.name = "target child"; //改变target的child
console.log(source.child.name); //child
console.log(target.child.name); //target child

//例2
var source = {
name: function () {
console.log(1);
},
child: { name: "child" }
};

var target = JSON.parse(JSON.stringify(source));
console.log(target.name); //undefined

//例3
var source = {
name: function () {
console.log(1);
},
child: new RegExp("e")
};

var target = JSON.parse(JSON.stringify(source));
console.log(target.name); //undefined
console.log(target.child); //Object {}

缺点

  • 这种方法能正确处理的对象只有 NumberStringBooleanArray 扁平对象,即那些能够被 json 直接表示的数据结构。
  • 对于正则表达式类型(RegExp 对象)、函数类型等无法进行深拷贝(而且会直接丢失相应的值)。
  • 会抛弃对象的 constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成 Object。同时如果对象中存在循环引用的情况也无法正确处理。

jQuery.extend()方法源码实现

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

  • deep 表示是否深拷贝,为 true 为深拷贝,为 false,则为浅拷贝

  • target Object 类型 目标对象,其他对象的成员属性将被附加到该对象上。

  • object1 objectN 可选。 Object 类型 第一个以及第 N 个被合并的对象。

1
2
3
4
5
let a = [0, 1, [2, 3], 4],
b = $.extend(true, [], a);
a[0] = 1;
a[2][0] = 1;
console.log(a, b); // [1, 1, [1, 3], 4] [0, 1, [2, 3], 4]

lodash 函数库实现深拷贝

lodash 很热门的函数库,提供了 lodash.cloneDeep()实现深拷贝