2024-08-27 10:14:31 +08:00

533 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 深浅拷贝
## 经典真题
- 深拷贝和浅拷贝的区别?如何实现
## 深拷贝和浅拷贝概念
首先,我们需要明确深拷贝和浅拷贝的概念。
- **浅拷贝**:只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做浅拷贝(浅复制)。浅拷贝只复制指向某个对象的指针(引用地址),而不复制对象本身,新旧对象还是共享同一块内存。
- **深拷贝**:在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象是完全隔离,互不影响。
## 浅拷贝方法
接下来我们来看一下对象有哪些浅拷贝方法。
**1. 直接赋值**
直接赋值是最常见的一种浅拷贝方式。例如:
```js
var stu = {
name: 'xiejie',
age: 18
}
// 直接赋值
var stu2 = stu;
stu2.name = "zhangsan";
console.log(stu); // { name: 'zhangsan', age: 18 }
console.log(stu2); // { name: 'zhangsan', age: 18 }
```
**2. *Object.assign* 方法**
我们先来看一下 *Object.assign* 方法的基本用法。
该方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。
如下:
```js
var stu = {
name: 'xiejie'
}
var stu2 = Object.assign(stu, { age: 18 }, { gender: 'male' })
console.log(stu2); // { name: 'xiejie', age: 18, gender: 'male' }
```
在上面的代码中,我们有一个对象 *stu*,然后使用 *Object.assign* 方法将后面两个对象的属性值分配到 *stu* 目标对象上面。
最终得到 *{ name: 'xiejie', age: 18, gender: 'male' }* 这个对象。
通过这个方法,我们就可以实现一个对象的拷贝。例如:
```js
const stu = {
name: 'xiejie',
age: 18
}
const stu2 = Object.assign({}, stu)
stu2.name = 'zhangsan';
console.log(stu); // { name: 'xiejie', age: 18 }
console.log(stu2); // { name: 'zhangsan', age: 18 }
```
在上面的代码中,我们使用 *Object.assign* 方法来对 *stu* 方法进行拷贝,并且可以看到修改拷贝后对象的值,并没有影响原来的对象,这仿佛实现了一个深拷贝。
然而,*Object.assign* 方法事实上是一个浅拷贝。
当对象的属性值对应的是一个对象时,该方法拷贝的是对象的属性的引用,而不是对象本身。
例如:
```js
const stu = {
name: 'xiejie',
age: 18,
stuInfo: {
No: 1,
score: 100
}
}
const stu2 = Object.assign({}, stu)
stu2.name = 'zhangsan';
stu2.stuInfo.score = 90;
console.log(stu); // { name: 'xiejie', age: 18, stuInfo: { No: 1, score: 90 } }
console.log(stu2); // { name: 'zhangsan', age: 18, stuInfo: { No: 1, score: 90 } }
```
**3. *ES6* 扩展运算符**
首先我们还是来回顾一下 *ES6* 扩展运算符的基本用法。
ES6 扩展运算符可以将数组表达式或者 *string* 在语法层面展开,还可以在构造字面量对象时,将对象表达式按 *key-value* 的方式展开。
例如:
```js
var arr = [1, 2, 3];
var arr2 = [3, 5, 8, 1, ...arr]; // 展开数组
console.log(arr2); // [3, 5, 8, 1, 1, 2, 3]
var stu = {
name: 'xiejie',
age: 18
}
var stu2 = { ...stu, score: 100 }; // 展开对象
console.log(stu2); // { name: 'xiejie', age: 18, score: 100 }
```
接下来我们来使用扩展运算符来实现对象的拷贝,如下:
```js
const stu = {
name: 'xiejie',
age: 18
}
const stu2 = {...stu}
stu2.name = 'zhangsan';
console.log(stu); // { name: 'xiejie', age: 18 }
console.log(stu2); // { name: 'zhangsan', age: 18 }
```
但是和 *Object.assign* 方法一样,如果对象中某个属性对应的值为引用类型,那么直接拷贝的是引用地址。如下:
```js
const stu = {
name: 'xiejie',
age: 18,
stuInfo: {
No: 1,
score: 100
}
}
const stu2 = {...stu}
stu2.name = 'zhangsan';
stu2.stuInfo.score = 90;
console.log(stu); // { name: 'xiejie', age: 18, stuInfo: { No: 1, score: 90 } }
console.log(stu2); // { name: 'zhangsan', age: 18, stuInfo: { No: 1, score: 90 } }
```
**4. 数组的 *slice* 和 *concat* 方法**
*javascript* 中,数组也是一种对象,所以也会涉及到深浅拷贝的问题。
*Array* 中的 *slice**concat* 方法,不修改原数组,只会返回一个浅复制了原数组中的元素的一个新数组。
例如:
```js
// concat 拷贝数组
var arr1 = [1, true, 'Hello'];
var arr2 = arr1.concat();
console.log(arr1); // [ 1, true, 'Hello' ]
console.log(arr2); // [ 1, true, 'Hello' ]
arr2[0] = 2;
console.log(arr1); // [ 1, true, 'Hello' ]
console.log(arr2); // [ 2, true, 'Hello' ]
```
```js
// slice 拷贝数组
var arr1 = [1, true, 'Hello'];
var arr2 = arr1.slice();
console.log(arr1); // [ 1, true, 'Hello' ]
console.log(arr2); // [ 1, true, 'Hello' ]
arr2[0] = 2;
console.log(arr1); // [ 1, true, 'Hello' ]
console.log(arr2); // [ 2, true, 'Hello' ]
```
但是,这两个方法仍然是浅拷贝。如果一旦涉及到数组里面的元素是引用类型,那么这两个方法是直接拷贝的引用地址。如下:
```js
// concat 拷贝数组
var arr1 = [1, true, 'Hello', { name: 'xiejie', age: 18 }];
var arr2 = arr1.concat();
console.log(arr1); // [ 1, true, 'Hello', { name: 'xiejie', age: 18 } ]
console.log(arr2); // [ 1, true, 'Hello', { name: 'xiejie', age: 18 } ]
arr2[0] = 2;
arr2[3].age = 19;
console.log(arr1); // [ 1, true, 'Hello', { name: 'xiejie', age: 19 } ]
console.log(arr2); // [ 2, true, 'Hello', { name: 'xiejie', age: 19 } ]
```
```js
// concat 拷贝数组
var arr1 = [1, true, 'Hello', { name: 'xiejie', age: 18 }];
var arr2 = arr1.slice();
console.log(arr1); // [ 1, true, 'Hello', { name: 'xiejie', age: 18 } ]
console.log(arr2); // [ 1, true, 'Hello', { name: 'xiejie', age: 18 } ]
arr2[0] = 2;
arr2[3].age = 19;
console.log(arr1); // [ 1, true, 'Hello', { name: 'xiejie', age: 19 } ]
console.log(arr2); // [ 2, true, 'Hello', { name: 'xiejie', age: 19 } ]
```
**5. *jQuery* 中的 *$.extend***
*jQuery* 中,*$.extend(deep,target,object1,objectN)* 方法可以进行深浅拷贝。各参数说明如下:
- *deep*:如过设为 *true* 为深拷贝,默认是 *false* 浅拷贝
- *target*:要拷贝的目标对象
- *object1*:待拷贝到第一个对象的对象
- *objectN*待拷贝到第N个对象的对象
来看一个具体的示例:
```js
<body>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>
const obj = {
name: 'wade',
age: 37,
friend: {
name: 'james',
age: 34
}
}
const cloneObj = {};
// deep 默认为 false 为浅拷贝
$.extend(cloneObj, obj);
obj.friend.name = 'rose';
console.log(obj);
console.log(cloneObj);
</script>
</body>
```
效果:
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-08-31-053219.png" alt="image-20210831133219541" style="zoom:50%;" />
## 深拷贝方法
说完了浅拷贝,接下来我们来看如何实现深拷贝。
总结一下,大致有如下的方式。
**1. *JSON.parse(JSON.stringify)***
这是一个广为流传的深拷贝方式,用 *JSON.stringify* 将对象转成 *JSON* 字符串,再用 *JSON.parse* 方法把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。
示例如下:
```js
const stu = {
name: 'xiejie',
age: 18,
stuInfo: {
No: 1,
score: 100
}
}
const stu2 = JSON.parse(JSON.stringify(stu));
stu2.name = 'zhangsan';
stu2.stuInfo.score = 90;
console.log(stu); // { name: 'xiejie', age: 18, stuInfo: { No: 1, score: 100 } }
console.log(stu2); // { name: 'zhangsan', age: 18, stuInfo: { No: 1, score: 90 } }
```
这种方式看似能够解决问题,但是这种方法也有一个缺点,那就是不能处理函数。
这是因为 *JSON.stringify* 方法是将一个 *javascript* 值(对象或者数组)转换为一个 *JSON* 字符串,而 *JSON* 字符串是不能够接受函数的。同样,正则对象也一样,在 *JSON.parse* 解析时会发生错误。
例如:
```js
const stu = {
name: 'xiejie',
age: 18,
stuInfo: {
No: 1,
score: 100,
saySth: function () {
console.log('我是一个学生');
}
}
}
const stu2 = JSON.parse(JSON.stringify(stu));
stu2.name = 'zhangsan';
stu2.stuInfo.score = 90;
console.log(stu); // { name: 'xiejie', age: 18, stuInfo: { No: 1, score: 100, saySth: [Function: saySth] }}
console.log(stu2); // { name: 'zhangsan', age: 18, stuInfo: { No: 1, score: 90 } }
```
可以看到,在原对象中有方法,拷贝之后,新对象中没有方法了。
**2. *$.extend(deep,target,object1,objectN)***
前面在介绍浅拷贝时提到了 *jQuery* 的这个方法,该方法既能实现浅拷贝,也能实现深拷贝。要实现深拷贝,只需要将第一个参数设置为 *true* 即可。例如:
```js
<body>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script>
const obj = {
name: 'wade',
age: 37,
friend: {
name: 'james',
age: 34
}
}
const cloneObj = {};
// deep 设为 true 为深拷贝
$.extend(true, cloneObj, obj);
obj.friend.name = 'rose';
console.log(obj);
console.log(cloneObj);
</script>
</body>
```
效果:
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2021-08-31-054115.png" alt="image-20210831134114926" style="zoom:50%;" />
**3. 手写递归方法**
最终,还是只有靠我们自己手写递归方法来实现深拷贝。
示例如下:
```js
function deepClone(target) {
var result;
// 判断是否是对象类型
if (typeof target === 'object') {
// 判断是否是数组类型
if (Array.isArray(target)) {
result = []; // 如果是数组,创建一个空数组
// 遍历数组的键
for (var i in target) {
// 递归调用
result.push(deepClone(target[i]))
}
} else if (target === null) {
// 再判断是否是 null
// 如果是,直接等于 null
result = null;
} else if (target.constructor === RegExp) {
// 判断是否是正则对象
// 如果是,直接赋值拷贝
result = target;
} else if (target.constructor === Date) {
// 判断是否是日期对象
// 如果是,直接赋值拷贝
result = target;
} else {
// 则是对象
// 创建一个空对象
result = {};
// 遍历该对象的每一个键
for (var i in target) {
// 递归调用
result[i] = deepClone(target[i]);
}
}
} else {
// 表示不是对象类型,则是简单数据类型 直接赋值
result = target;
}
// 返回结果
return result;
}
```
在上面的代码中,我们封装了一个名为 *deepClone* 的方法,在该方法中,通过递归调用的形式来深度拷贝一个对象。
下面是 *2* 段测试代码:
```js
// 测试1
const stu = {
name: 'xiejie',
age: 18,
stuInfo: {
No: 1,
score: 100,
saySth: function () {
console.log('我是一个学生');
}
}
}
const stu2 = deepClone(stu)
stu2.name = 'zhangsan';
stu2.stuInfo.score = 90;
console.log(stu); // { name: 'xiejie', age: 18, stuInfo: { No: 1, score: 100, saySth: [Function: saySth] }}
console.log(stu2); // { name: 'xiejie', age: 18, stuInfo: { No: 1, score: 90, saySth: [Function: saySth] }}
```
```js
// 测试2
var arr1 = [1, true, 'Hello', { name: 'xiejie', age: 18 }];
var arr2 = deepClone(arr1)
console.log(arr1); // [ 1, true, 'Hello', { name: 'xiejie', age: 18 } ]
console.log(arr2); // [ 1, true, 'Hello', { name: 'xiejie', age: 18 } ]
arr2[0] = 2;
arr2[3].age = 19;
console.log(arr1); // [ 1, true, 'Hello', { name: 'xiejie', age: 18 } ]
console.log(arr2); // [ 2, true, 'Hello', { name: 'xiejie', age: 19 } ]
```
## 真题解答
- 深拷贝和浅拷贝的区别?如何实现
> 参考答案:
>
> - **浅拷贝**:只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做浅拷贝(浅复制)
>
> 浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
>
> - **深拷贝**:在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象是完全隔离,互不影响。
>
> **浅拷贝方法**
>
> 1. 直接赋值
> 2. *Object.assign* 方法:可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。当拷贝的 *object* 只有一层的时候,是深拷贝,但是当拷贝的对象属性值又是一个引用时,换句话说有多层时,就是一个浅拷贝。
> 3. *ES6* 扩展运算符,当 *object* 只有一层的时候,也是深拷贝。有多层时是浅拷贝。
> 4. *Array.prototype.concat* 方法
> 5. *Array.prototype.slice* 方法
> 6. *jQuery* 中的 *$.extend*:在 *jQuery* 中,*$.extend(deep,target,object1,objectN)* 方法可以进行深浅拷贝。*deep* 如过设为 *true* 为深拷贝,默认是 *false* 浅拷贝。
>
> **深拷贝方法**
>
> 1. *$.extend(deep,target,object1,objectN)*,将 *deep* 设置为 *true*
> 2. *JSON.parse(JSON.stringify)*:用 *JSON.stringify* 将对象转成 *JSON* 字符串,再用 *JSON.parse* 方法把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。这种方法虽然可以实现数组或对象深拷贝,但不能处理函数。
> 3. 手写递归
>
> 示例代码如下:
>
> ```js
> function deepCopy(oldObj, newobj) {
> for (var key in oldObj) {
> var item = oldObj[key];
> // 判断是否是对象
> if (item instanceof Object) {
> if (item instanceof Function) {
> newobj[key] = oldObj[key];
> } else {
> newobj[key] = {}; //定义一个空的对象来接收拷贝的内容
> deepCopy(item, newobj[key]); //递归调用
> }
>
> // 判断是否是数组
> } else if (item instanceof Array) {
> newobj[key] = []; //定义一个空的数组来接收拷贝的内容
> deepCopy(item, newobj[key]); //递归调用
> } else {
> newobj[key] = oldObj[key];
> }
> }
> }
> ```
-*EOF*-