在ES6的系列文章中,基本都会提到Spread
——扩展运算符(...
)。而在这个运算符的相关用例中,往往会涉及到其他知识点,深拷贝和浅拷贝就是其中之一。
背景知识
在讨论深拷贝和浅拷贝之前,我们先看一个例子:
let a = 'hi';
b = a;
b = 'hello';
console.log(a);
// 'hi'
let arr1 = [1,2,3];
arr2 = arr1;
arr2[0] = 0;
console.log(arr1);
// [0,2,3]
可以看到:为不同的js变量复制值时,结果是不同的。把字符串a
的值复制给b
,改变b
的值不会影响a
的值,而把数组arr1
的值复制给arr2
时,改变arr2
的值,arr1
的值也跟着改变了。
这是因为js存在两种不同数据类型的值:基本类型值
和引用类型值
。
在访问这两种类型的变量时,其访问方式是不同的。在这里,先记一下结论:
- 基本数据类型是按值访问的
- 引用数据类型是按引用访问的
(实际上这种说法并不严密,为便于理解,我们先这么记)
什么意思?
JavaScript不允许直接访问内存中的位置,换句话说,不能直接操作对象的内存空间。
因此,在操作对象时,我们实际上是在操作对象的引用,而不是实际的对象。
从一个变量向另一个变量复制值时(不管是复制基本类型还是引用类型),都会先为这个新变量分配一个空间,然后把该值复制到新创建的这个空间里。
不同的是,在复制引用类型的值时,实际上复制过去的是一个指针,示例图如下:
在js中,除了7种基本类型外,其他的都是引用类型,比如Object
,Array
,Function
,所以不难理解:
let obj1 = {name:'hx',age:18};
let obj2 = obj1;
obj2.age = 0;
console.log(obj1);
// {name:'hx',age:0}
“引用类型的值是按引用访问的”不严密在:当复制保存着对象的某个变量时,操作的是对象的引用;而当为对象添加属性时,操作的是实际的对象。 ——灵图社区
深拷贝 vs. 浅拷贝
我们先来看一下概念:
浅拷贝:
被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。即对象的浅拷贝会对主对象的值进行拷贝,而该值有可能是一个指针,指向内存中的同一个对象。
深拷贝:
深拷贝不仅将原对象的各个属性逐个复制出去,而且将原对象各个属性所包含的对象也依次采用深复制的方法递归复制到新对象上。所以对一个对象的修改完全不会影响到另一个对象。
OK,可以看到深拷贝和浅拷贝是对“复制引用类型变量”而言的。事实上,也只有在引用类型中才有讨论两者区别的意义,对于基本数据类型,怎么拷都是“深拷贝”。
浅拷贝就不说了,=
就是浅拷贝,那么如何实现深拷贝呢?
对于Object
和Array
两种类型,我们分别举例:
Object
首先是assign()
,看代码:
let obj = {name:'hx',age:18};
let copyObj = Object.assign({},obj);
copyObj.name = 'H.Lucas';
console.log(obj);
// {name:'hx', age:18}
Emm,貌似是深拷贝哈,那要是二维对象呢?
let obj = {name:'zj',attr:{age:18, nickname:'Z.Crystal'}}
let copyObj = Object.assign({},obj);
copyObj.attr.nickname = 'erni';
console.log(obj);
// {name:'zj',attr:{age:18, nickname:'erni'}}
好吧,翻车了,看来assign只能实现一维对象的深拷贝。
然后是扩展运算符...
,看代码:
let obj = {name:'hx',age:18};
let copyObj = {...obj};
copyObj.name = 'H.Lucas';
console.log(obj);
// {name:'hx', age:18}
嗯,深拷贝哈,也来个二维对象试试:
let obj = {name:'zj',attr:{age:18, nickname:'Z.Crystal'}}
let copyObj = {...obj};
copyObj.attr.nickname = 'erni';
console.log(obj);
// {name:'zj',attr:{age:18, nickname:'erni'}}
好吧,也炸了,看来都实现不了多维对象的深拷贝。
不过这里还是推崇一下...
,为什么?看两段代码:
let obj1 = {a:1,b:2}
let obj2 = {b:3,c:4}
// 构建一个新对象obj,值是obj1和obj2的集合
let obj = Object.assign(obj1,obj2)
obj.b = 100
console.log(obj1)
console.log(obj2)
// {a: 1, b: 100, c: 4}
// {b: 3, c: 4}
let obj1 = {a:1,b:2}
let obj2 = {b:3,c:4}
// 构建一个新对象obj,值是obj1和obj2的集合
let obj = {...obj1,...obj2}
obj.b = 100
console.log(obj1)
console.log(obj2)
// {a: 1, b: 2}
// {b: 3, c: 4}
在第一段代码中,执行完Object.assign()时,obj1已经改变了,而且改变组合出来的obj时,obj1还会再改变。实际上我只想组合出一个完全独立的obj来,可以肆意改变它,而不影响原始数据(想一下纯函数的实现,以及Redux等)。
Array
Emm,数组拷贝能想到哪些?slice()
,concat()
,Array.from()
,...
这里就不一个一个试了,先给出结论吧:
都只能实现一维数组的深拷贝
看个例子:
let arr1 = [1, 2], arr2 = [...arr1];
console.log(arr1); // [1, 2]
console.log(arr2); // [1, 2]
arr2[0] = 3;
console.log(arr1); // [1, 2]
console.log(arr2); // [3, 2]
let arr3 = [1, 2, [3, 4]], arr4 = [...arr3];
console.log(arr3); // [1, 2, [3, 4]]
console.log(arr4); // [1, 2, [3, 4]]
arr4[2][1] = 5;
console.log(arr3); // [1, 2, [3, 5]]
console.log(arr4); // [1, 2, [3, 5]]
好吧,那js里到底有没有不限条件的深拷贝方法呢?看下这个:
let obj1 = {x:1, y:{z:1}};
let obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj2)
// {x:1, y:{z:1}}
// 改一下obj2,看看会不会影响obj1
obj2.y.z = 2;
console.log(obj1)
// {x:1, y:{z:1}}
// 可以,obj1没有受到obj2的影响
简单粗暴吧?不过JSON.parse(JSON.stringify())
也并不是万能的,比如对象的属性是undefined,function()时:
let obj1 = {
x: 1,
y: undefined,
z: () => console.log('lalala')
};
let obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj2);
// {x: 1}
// 源对象的属性y和z都丢失了,更别说深拷贝了
那数组呢?有没有不限条件的深拷贝方法,哪怕有个类似的简单粗暴的不完全体也行啊。
这里提供一种思路:
var deepClone = function(obj) {
// 如果是null,或不是Object和Array类型,直接返回
if (typeof obj == null || obj !== object) {
return obj;
}
let result;
if (obj instanceof Array) {
result = [];
} else {
result = {};
}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 递归调用
result[key] = deepClone(result[key]);
}
}
// 返回拷贝的结果
return result
}
Summary
-
JavaScript有两种数据类型:基本数据类型和引用数据类型;
- 基本数据类型:String,Number,Boolean,Undefined,Null,Symbol
- 引用数据类型:Object,Array,Function
- JavaScript不允许直接访问内存中的位置,因此我们操作的只是对象引用,而不是实际的对象;
- 深拷贝和浅拷贝是针对引用数据类型而言的;
- JavaScript暂时还没有实现多维数组/对象深拷贝的内置方法(还是说,有,但是我不知道。。)
对了,补充一个知识点:
有人说,我家=
也可以实现一级深拷贝啊,你看:let arr1 = {a:1,b:2}; arr2 = arr1; arr2 = {a:0,b:1}; console.log(arr2); // {a:0, b:2} console.log(arr1); // {a:1, b:2}
额。。这是另一种典型错误了。。
arr2 = {a:0,b:1};
vs.arr2.a = 0;
这两种操作可不是一码事。。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。