felix-cao / Blog

A little progress a day makes you a big success!
31 stars 4 forks source link

JavaScript 浅拷贝(Shallow Copy)与深拷贝(Deep Copy) #153

Open felix-cao opened 5 years ago

felix-cao commented 5 years ago

浅拷贝 (Shallow Copy) 与深拷贝 (Deep Copy),对于像 ObjectArray 以及 ES6 新增的 SetMap 这样的聚合对象才有实际意义,不同的是浅拷贝只复制聚合对象的第一层属性,深拷贝可以对对象的所有属性进行递归复制

一、JavaScript 的两种变量数据类型

JavaScript 变量的数据类型分为基本数据类型引用数据类型

基本类型是按值访问的,不会影响到其他数据

var name = 'Felix'
var frontName  = name
name = 'Lucy'
frontName

上面的代码,将 name 赋值给 frontName 后再对 name 的值进行改变,并不会影响 frontName 的值,因为他们是基本数据类型,不会影响到其他数据。

所以按值访问的基本数据类型是没有深浅拷贝之别。

而引用类型的值是按地址访问的,简单的赋值,实际上只是把地址复制了一遍,修改任意一个值会影响到另外一个,例如

var people = { name: 'Felix', age: 18 }
var student = people;
student.name = 'student'
console.log(people.name) // student

上面的代码可以看到,people 对象赋值给了 student 对象,JavaScript引擎只是将 people对象 的地址赋值给了 student,他们指向同一个内存地址,并没有开辟新的栈,当修改 peoplestudent 对象中属性的值时,另外一个也被影响了,这就是浅拷贝!但是很多时候我们并不希望这样。

二、浅拷贝 (shallow copy)

浅拷贝是对对象地址的拷贝,并没有开辟新的栈,复制的结果是两个对象指向同一个地址,修改其中一个对象的属性,另外一个对象的属性也会改变。

浅拷贝的实现

2.1、直接赋值

聚合对象变量的直接赋值是一种浅拷贝。如下:

var m = { a: 10, b: 20 }
var n = m;
n.a = 15;
console.log(m.a) // 15

上面的代码直接通过赋值的方式,是一种常见的浅拷贝

2.2、数组的 Array.concat() 函数

Array.concat(array) 方法会返回一个新的数组

var arr1 = ['a', 'b', 'c'];
var arr2 = ['d', 'e', 'f'];
var coll = arr1.concat(arr2);
arr1[0] = '被修改了';
console.log(arr1); // ["被修改了", 2, 3]
console.log(coll) // [1, 2, 3, 4, 5, 6]

上面的代码,修改数组 arr1 并不会影响合并后的数组 coll 的值,这种表面现象误导了我很常一段时间,让我以为 concat 方法是深拷贝,后来阅读了MDN文档

concat 方法不会改变 this 或任何作为参数提供的数组,而是返回一个浅拷贝,它包含与原始数组相结合的相同元素的副本。 原始数组的元素将复制到新数组中,如下所示:

简单的说,Array.concat(array) 方法是浅拷贝,但数组的元素为基本数据类型时会造成深拷贝的假象。

var arr1 = [{name: '小红'}];
var arr2 = [{name: '小明'}];
var coll = arr1.concat(arr2);
console.log(coll);  // [{name:'小红'},{name:'小明'}]
arr1[0].name = '被修改了';
console.log(coll); // [{name:'被修改了'},{name:'小明'}]

上面的代码可以看出修改 arr1 下标0对应的对象,coll 的值也被影响了。他们指向同一个地址,所以concat 是浅拷贝,另外,Array.slice 方法和 concat 一样不会影响原数组,但同样是浅拷贝。

2.3、对象的浅拷贝

Object.assign() 方法用于将所有可枚举的属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

Object.assign(target, ...sources)

他同样会让人产生困扰,如果对象深度只有1层,Object.assign() 产生的对象和 concat 一样,不会影响其他对象,例如

var obj1 = { a: 1, b: 2 };
var obj2 = { A: 1, B: 2 };
var coll = Object.assign({}, obj1, obj2);
obj1.a = '被修改了';
console.log(coll); // {a: 1, b: 2, A: 1, B: 2}

修改 obj1.a 的属性不会影响 coll,是因为 obj.a 对应的 数字 1 是基本类型,按值访问。如果obj1.a 对应引用类型的值,复制的就是这个引用类型的地址。

var obj1 = { a: { name: '小红' }, b: 2 };
var obj2 = { A: 1, B: 2 };
var coll = Object.assign({}, obj1, obj2);
obj1.a.name = '被修改了';
console.log(coll);  // {"a":{"name":"被修改了"},"b":2,"A":1,"B":2}

三、深拷贝 (deep copy) 的实现

深拷贝可以通过JSON序列号后再解析回来的方式实现 原理: 用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,一来一回之间,新的对象产生了,而且对象会开辟新的栈,实现深拷贝

var obj1 = { a: { name: '小红' }, b: 2 };
var obj2 = JSON.parse(JSON.stringify(obj1));
obj1.a.name = '被修改了';
obj2   //{"a":{"name":"小红"},"b":2}  《---没有被修改

四、应用场景

深拷贝在实际的业务中是最常见的应用场景,比如从服务器fetch到数据之后我将其存放在store中,通过props传递给界面,然后我需要对这堆数据进行修改,那涉及到修改就一定有保存和取消,所以我们需要将这堆数据拷贝到其他地方。