浅谈深拷贝与浅拷贝

一开始看到深拷贝与浅拷贝我是完全一脸懵逼的,之前只在掘金的热门文章看到过,不过当时也没深究。

最近项目有个需求才让我真正认识深拷贝,项目需求是这样的,有一个用来存储element多选下拉框选中的值的数组,传往后台是后台需要字符串,于是机智的我立马加上一个join(',')然后这边传
往后台,万万没想到,因为我用了element的rules来验证表单是否非空的,将数组改成字符串之后验证就不通过了,因为element的验证还会验证数据的类型。然后我就准备用一个临时变量来存储表单数据,然后修改临时变量里面的数组(将其变成字符串),改好后准备再次提交时,又验证失败了,当时又一脸懵逼了,为什么这样也报错。于是百度了一翻知道了深拷贝。

1、 为什么修改临时变量form表单的数据也会修改?

下面来一个demo

1
2
3
4
5
let arr = [1, 2, 3, 4];
let arr1 = arr;
arr1[0] = 'a';
console.log(arr);
console.log(arr1);

看到这个例子,大多数人的答案肯定是arr是[1, 2, 3, 4],arr1是["a", 2, 3, 4],然而情况确是

再来一个demo

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

有了上面例子的经验,机智的小伙伴们一定以为输出的都是2,然而

简单粗暴的回答就是:数组是引用类型,修改数组中值会修改内存地址中的数据。

下面的详细的讲解以下:

1.1数据类型

javascript中有基本类型和引用类型两种数据类型

5种基本类型分别为Undefined、Null、Boolean、Number 和 String,变量是直接按值存放的,存放在栈内存中的简单数据段,可以直接访问。

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

所以赋值时,基本类型是直接复制了一个新的变量,新旧两个变量之间没有关系;而引用类型也复制了新的变量,但这个变量是一个指针,新旧两个指针指向同一个对象,所以修改临时变量也会改动原有的form表单的数据

2、如果想实现项目里面的那个需求的话要怎么做?

2.1深拷贝的实现

2.1.1利用JSON对象来实现最简单的深拷贝

1
2
3
4
5
let arr = [1, 2, 3, 4];
let arr1 = JSON.parse(JSON.stringify(arr));
arr1[0] = 'a';
console.log(arr)
console.log(arr1)

输出为

这种方法使用较为简单,可以满足基本的深拷贝需求,而且能够处理JSON格式能表示的所有数据类型,但是对于正则表达式类型、函数类型等无法进行深拷贝(而且会直接丢失相应的值)。还有一点不好的地方是它会抛弃对象的constructor。也就是深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object。同时如果对象中存在循环引用的情况也无法正确处理。

1
2
3
4
5
6
7
8
9
10
11
// 来一个JSON深拷贝的坑的示例
const obj = {
    arr: [111, 222],
    obj: {key: '对象'},
    a: () => {console.log('函数')},
    date: new Date(),
    reg: /正则/ig
};
let obj1 = JSON.parse(JSON.stringify(obj));
console.log(obj);
console.log(obj1);

输出为
可以从中看出,obj中的普通对象和数组都能拷贝,然而date对象成了字符串,函数直接就不见了,正则成了一个空对象。

2.1.2 for in 实现深拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function deepClone(source){
if(!source || typeof source !== 'object'){
throw new Error('数据不是引用类型');
}
var targetObj = source.constructor === Array ? [] : {};
for(var keys in source){
if(source.hasOwnProperty(keys)){
if(source[keys] && typeof source[keys] === 'object'){
targetObj[keys] = source[keys].constructor === Array ? [] : {};
targetObj[keys] = deepClone(source[keys]);
}else{
targetObj[keys] = source[keys];
}
}
}
return targetObj;
}
let arr = [1,2,3,4];
let arr1 = deepClone(arr);
arr1[0] = 'a';
console.log(arr)
console.log(arr1)

输出为

3、拓展,浅拷贝的实现

3.1 最简单的浅拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 对象
var o1 = {a: 1};
var o2 = o1;

console.log(o1 === o2); // =>true
o2.a = 2;
console.log(o1.a); // => 2

// 数组
var o1 = [1,2,3];
var o2 = o1;

console.log(o1 === o2); // => true
o2.push(4);
console.log(o1); // => [1,2,3,4]

拷贝原对象的引用,这是最简单的浅拷贝。

3.2 Object.assign()

Object.assign是ES6的新函数。Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。但是 Object.assign() 进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身。

1
2
3
4
5
let arr = [1,2,3,4,5];
let arr1 = Object.assign([], arr)
arr1[0] = 'a'
console.log(arr) // [1, 2, 3, 4, 5]
console.log(arr1) //  ["a", 2, 3, 4, 5]

3.3 扩展运算符&解构赋值

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
const arr = [1, 2, 3];
const newArr = [...arr]
arr.push(4)
console.log(arr); // [1, 2, 3, 4]
console.log(newArr); // [1, 2, 3]


const a = {
name: '1',
address: {
province: 'sichuan',
city: 'chengdu'
}
}

let {
name,
address
} = a

name = 'myname'
address.province = 'shanghai'
address.city = 'shanghai'

console.log(name, address) // myname {province: 'shanghai', city: 'shanghai'}
console.log(a) // { address: {province: 'shanghai', city: 'shanghai'} name: "1" }

4、深拷贝与浅拷贝的区别

浅拷贝是复制,两个对象指向同一个地址;

深拷贝是新开栈,两个对象指向不同的地址

| 和原数据是否指向同一对象 | 第一层数据为基本数据类型 | 原数据中包含子对象
:- | :- | :- | :-
赋值 | 是 | 改变会使原数据一起改变 | 改变会使原数据一起改变
浅拷贝 | 否 | 不会改变原数据 | 改变会使原数据一起改变
深拷贝 | 否 | 不会改变原数据 | 不会改变原数据

参考资料:
javaScript中浅拷贝和深拷贝的实现
JavaScript中的浅拷贝和深拷贝
再谈Javascript中的基本类型和引用类型(推荐)
JavaScript深拷贝的一些坑


浅谈深拷贝与浅拷贝
https://xypecho.github.io/2018/07/02/浅谈深拷贝与浅拷贝/
作者
很青的青蛙
发布于
2018年7月2日
许可协议