前言

此文部分来自于公众号作者 大迁世界 本站只做记录使用,如有侵权,请联系站长删除!

所谓深浅拷贝,其实都是进行复制,主要区别在于复制出来的新对象和原来的对象时候会相互影响。
深浅拷贝的区分:B 复制 A,如何A 发生变化 B跟着变化 浅拷贝
反之, 如果 B 不发生变化 则为深拷贝。
浅拷贝 A B 指向同一个地址 所以 A受影响 B也会受影响

1. 深浅拷贝实例

浅拷贝

1
2
3
4
5
6
var a =[1,2,3,4]
var a_clone = a;
console.log(a === a_clone);
a_clone[0] = 5;
console.log(a_clone);
console.log(a);

深拷贝

1
2
3
4
5
var obj=[1,2,3,[4,5]];
var obj_extend =$.extend(true,{},obj);
console.log(obj_extend === obj);
console.log(obj_extend);
console.log(obj);

要深入了解深浅拷贝必选先要了解其原理,这就不得不说到ECMAScript中的数据类型。

2. 基本类型和引用类型

2.1 分类

基本类型:undefined,null,布尔值(Boolean),字符串(String),数值(Number)
引用类型:统称为Object类型,细分为:Function、Date、Array、Object类型等。

2.2 不同的数据存储形式

简单来说, 基本数据类型保存在栈内存 引用类型保存在堆内存

2.2.1 栈内存

栈内存中分别存储着变量的标识符以及变量的值

即:var a = "A"

在栈内存中是这样的

2.1.2 堆内存

栈内存存储的是变量的标识符以及对象在堆内存中的存储地址,但需要访问引用类型(对象、数组等)的值时,首先需要从栈中获得该对象的地址指针,然后再从对应的堆内存中取得所需的数据。

即:var a = {name:“jack”};
在内存中是这样的:

3. 不同类型的复制形式

3.1 基本类型的复制

当你在复制时,相当于把值也一并复制给了新的变量

1
2
3
4
5
6
var a = 1;
var b = a;
console.log(a === b);
var a = 2;
console.log(a);
console.log(b);

改变a的值,并不会影响到b。

1
var a =1

1
var b = a;

1
a = 2;

3.2 引用类型的复制

当复制引用类型时,实际上只是赋值起指向的内存地址,即原来的变量与复制的新变量指向了同一个东西

1
2
3
4
5
6
var a ={name:"jack",age:20};
var b = a;
console.log(a === b);
a.age =30;
console.log(a);
console.log(b)

可以看得出来:改变a的值,也会影响b的值
内存中是这样的

1
var a = {name:"jack",age:20}

1
var b = a;

1
a.age = 30;

看完上面之后,是不是就明白了。所谓深浅拷贝:
对于仅仅是复制了引用(地址),换言之,原来的变量和新的变量指向了同一个东西,彼此之间的操作会互相影响,为浅拷贝
反言之。如果是在堆中重新分配内存,拥有不同的地址,但是值是一样的,复制后的对象与原来的对象是完全隔离,互不影响的,为 深拷贝

深浅拷贝的主要区别:复制的是引用(地址)还是复制的是实例。

接下来就对上述实例进行深拷贝的实现

4. 实现深拷贝方式

4.1 利用递归方式 对属性中所有的引用类型的值,遍历到是基本类型的值为止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function deepCLone(obj){
var targetObj = Array.isArray(obj)?[]:{};
for(var keys in obj){
if(obj.hasOwnProperty(keys)){
if(obj[keys] && typeof obj[keys] === 'object'){
targetObj[keys] = deepCLone(obj[keys])
}else{
targetObj[keys] = obj[keys]
}
}
}
return targetObj
}

var a ={name:"jack",age:20};
var b = deepCLone(a);
console.log(b ===a);
a.age = 30;
console.log(a);
console.log(b);

4.2 jQuery中的 extend复制方法

可以用来扩展对象,这个方法可以传入一个参数:deep(true or
false),表示是否执行深复制(如果执行深复制则会执行递归复制)

  • 深拷贝

    1
    2
    3
    4
    5
    6
    7
    var obj = {name:"xixi",age:20,company:{name:"腾讯",address:"深圳"}};
    var obj_extend =$.extend(true,{},obj);
    console.log(obj === obj_extend);
    obj.company.name="阿里";
    obj.name="albabba";
    console.log(obj);
    console.log(obj_extend);

  • 浅拷贝

    1
    2
    3
    4
    5
    6
    7
    var obj = {name:"xixi",age:20,company:{name:"腾讯",address:"深圳"}};
    var obj_extend =$.extend(false,{},obj);
    console.log(obj === obj_extend);
    obj.company.name="阿里";
    obj.name="albabba";
    console.log(obj);
    console.log(obj_extend);

从company的变化可以看出,貌似是浅拷贝,但是name又貌似是深拷贝,这是何解。

其实,Array的slice 和concat方法和jQuery中的extend复制方法,都是会复制第一层的值,对于第一层的值都是深拷贝,而到第二层的时候 Array的slice和concat方法就是复制引用,jQuery中的extend复制方法则取决于你的第一个参数,也就是时候进行递归复制。
所谓第一层 就是 key所对应的value值是基本数据类型,也就是像实例中的name,age,而对于value值是引用类型 则为第二层,也就是像上面的company。

顺便 我们可以试一下 slice 和concat

slice

1
2
3
4
5
6
var a =[1,2,3];
var b = a.slice();
console.log(b === a);
a[0] = 4;
console.log(a);
console.log(b);

concat

1
2
3
4
5
6
var a =[1,2,3];
var b = a.concat();
console.log(b === a);
a[0] = 4;
console.log(a);
console.log(b);

看似是深拷贝,其实不然,下面的例子就说明了一切

1
2
3
4
5
var a =[[1,2,3],4,5];
var b= a.slice();
a[0][0] = 6;
console.log(a);
console.log(b);

这就很明显的看出来了,上面所述,第一层是深拷贝 第二层 是引用。

4.3 JSON对象的parse和stringify

  • JSON对象中的stringify可以把一个js对象序列化为一个JSON字符串
  • parse可以把JSON字符串反序化为一个js对象,这两个方法实现的是深拷贝
1
2
3
4
5
6
7
var obj ={name:'xixi',age:20,company:{name:'TX',address:'深圳'}};
var obj_json = JSON.parse(JSON.stringify(obj));
console.log(obj === obj_json);
obj.name="ali";
obj.company.name="AL";
console.log(obj);
console.log(obj_json);

5. 如何实现对循环引用的深拷贝

1
2
3
let obj = { name: '1', address: {x: 100, y: 100}}
obj.o = obj

obj中的属性o指向obj本身,在深拷贝过程中就会出现循环引用问题
如果还使用上述的算法,就会出现调用栈溢出问题

因此,要解决循环引用问题的话,需要使用一个Map/WeakMap来存放已经拷贝过的对象
key为旧对象,value为新对象
在先拷贝属性之前,先去map中查找是否已经被处理过,如果被处理过,直接返回value值
在拷贝属性之前,把新、旧对象保存到map中

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
function deepClone(obj = {}, hash = new WeakMap()) {
// 如果传入的是值类型/null,直接返回
if (typeof obj != 'object' || obj == null) {
return obj
}

if (hash.get(obj)) {
return hash.get(obj)
}

let result
if (result instanceof Array) {
result = []
} else {
result = {}
}

hash.set(obj, result)

for (const key in obj) {
if (Object.hasOwnProperty.call(obj, key)) {
result[key] = deepClone(obj[key], hash)
}
}

return result
}

1
2
3
4
5
6
let obj = { name: '1', address: {x: 100, y: 100}}
obj.o = obj
obj.address.z = obj
console.log(deepClone(obj))
obj.address.x = 140
console.log(obj)

6. structuredClone()

JavaScript中深拷贝对象的最简单方法,可以深度复制任何对象。

1
2
3
4
5
6
const obj = { name: 'Tari', friends: [{ name: 'Messi' }] };

const clonedObj = structuredClone(obj);

console.log(obj.name === clonedObj); // false
console.log(obj.friends === clonedObj.friends); // false

6.1 轻松克隆循环引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const car = {
make: 'Toyota',
};

// 👆 循环引用
car.basedOn = car;

const cloned = structuredClone(car);

console.log(car.basedOn === cloned.basedOn); // false

// 👇 循环引用被克隆
console.log(car === car.basedOn); // true

这是永远无法用JSON stringify/parse技巧实现的:

1
2
3
4
5
6
7
8
const car = {
make: 'Toyota',
};

car.basedOn = car;
const cloned = JSON.parse(JSON.stringify(car));
console.log(car.basedOn === cloned.basedOn);
console.log(car === car.basedOn);

6.2 可对不限层次的对象进行拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const obj = {
a: {
b: {
c: {
d: {
e: 'Coding Beauty',
},
},
},
},
};

const clone = structuredClone(obj);

console.log(clone.a.b.c.d === obj.a.b.c.d); // false
console.log(clone.a.b.c.d.e); // Coding Beauty

6.3 structuredClone()的局限性

6.3.1 无法克隆函数或方法

1
2
3
4
5
6
7
8
const car = {
make: 'Toyota',
move(){
console.log('vroom vroom...');
}
};
car.basedOn = car;
const cloned = structuredClone(car);

报错信息:"Failed to execute ‘structuredClone’ on ‘Window’: move()’ 表示在尝试使用 structuredClone 方法复制或序列化一个对象时遇到了问题。structuredClone 是一个用于深度复制对象的方法,它能够正确复制包括函数在内的所有值。

问题可能出现在 move() 函数上,因为在尝试序列化或复制该函数时遇到了问题。structuredClone 不能复制不可序列化的值,比如函数或 DOM 节点。

6.3.2 无法克隆DOM元素

1
<div id='test'></div>
1
2
3
4
const test = document.getElementById('test');

const testClone = structuredClone(test);
console.log(testClone);

6.3.3 不保留RegExplastIndex属性

虽然不会去克隆正则表达式,但这是值得注意的一点:

1
2
3
4
5
6
7
8
const regex = /beauty/g;
const str = 'Coding Beauty: JS problems are solved at Coding Beauty';

console.log(regex.index);
console.log(regex.lastIndex); // 7

const regexClone = structuredClone(regex);
console.log(regexClone.lastIndex); // 0

6.4 其他的限制

了解这些限制很重要,以避免使用该函数时出现意外行为。

部分克隆,部分移动

这是一个更复杂的情况。

你将内部对象从源对象转移到克隆对象,而不是复制。

这意味着源对象中没有留下任何可以改变的东西:

1
2
3
4
5
6
7
8
9
10
const uInt8Array = Uint8Array.from(
{ length: 1024 * 1024 * 16 },
(v, i) => i
);

const transferred = structuredClone(uInt8Array, {
transfer: [uInt8Array.buffer],
});

console.log(uInt8Array.byteLength); // 0

总的来说,structuredClone()是JavaScript开发者工具箱中的一个宝贵补充,使对象克隆比以往任何时候都更容易。