Javascript中的深浅拷贝
工作中经常会遇到需要复制 js 数据的时候,遇到 bug 时实在令人头疼;面试中也经常会被问到如何实现一个数据的深浅拷贝,但是你对其中的原理清晰吗?一起来看一下吧!
为什么会有深浅拷贝
想要更加透彻的理解为什么 js 会有深浅拷贝,需要先了解下 js 的数据类型有哪些,一般分为基本类型(number、string、null、undefined、boolean、symbol )和引用类型(对象、数组、函数)。
基本类型是不可变的,任何方法都无法改变一个基本类型的值,也不可以给基本类型添加属性或者方法。但是可以为引用类型添加属性和方法,也可以删除其属性和方法。
基本类型和引用类型在内存中的存储方式也大不相同,基本类型保存在栈内存中,而引用类型保存在堆内存中。为什么要分两种保存方式呢? 因为保存在栈内存的必须是大小固定的数据,引用类型的大小不固定,只能保存在堆内存中,但是我们可以把它的地址写在栈内存中以供我们访问。
说来这么多,我们来看个示例:
let num1 = 10;
let obj1 = {
name: "hh"
}
let num2 = num1;
let obj2 = obj1;
num2 = 20;
obj2.name = "kk";
console.log(num1); // 10
console.log(obj1.name); // kk
执行完这段代码,内存空间里是这样的:
可以看到 obj1 和 obj2 都保存了一个指向该对象的指针,所有的操作都是对该引用的操作,所以对 obj2 的修改会影响 obj1。
小结:
之所以会出现深浅拷贝,是由于 js 对基本类型和引用类型的处理不同。基本类型指的是简单的数据段,而引用类型指的是一个对象保存在堆内存中的地址,js 不允许我们直接操作内存中的地址,也就是说不能操作对象的内存空间,所以,我们对对象的操作都只是在操作它的引用而已。
在复制时也是一样,如果我们复制一个基本类型的值时,会创建一个新值,并把它保存在新的变量的位置上。而如果我们复制一个引用类型时,同样会把变量中的值复制一份放到新的变量空间里,但此时复制的东西并不是对象本身,而是指向该对象的指针。所以我们复制引用类型后,两个变量其实指向同一个对象,所以改变其中的一个对象,会影响到另外一个。
深浅拷贝
1. 浅拷贝
浅拷贝只是复制基本类型的数据或者指向某个对象的指针,而不是复制对象本身,源对象和目标对象共享同一块内存;若对目标对象进行修改,存在源对象被篡改的可能。
我们来看下浅拷贝的实现:
/* sourceobj 表示源对象
* 执行完函数,返回目标对象
*/
function shadowclone (sourceobj = {}) {
let targetobj = array.isarray(sourceobj) [] : {};
let copy;
for (var key in sourceobj) {
copy = sourceobj[key];
targetobj[key] = copy;
}
return targetobj;
}
// 定义 source
let sourceobj = {
number: 1,
string: 'source1',
boolean: true,
null: null,
undefined: undefined,
arr: [{name: 'arr1'}, 1],
func: () => 'sourcefunc1',
obj: {
string: 'obj1',
func: () => 'objfunc1'
}
}
// 拷贝sourceobj
let copyobj = shadowclone(sourceobj);
// 修改 sourceobj
copyobj.number = 2;
copyobj.string = 'source2';
copyobj.boolean = false;
copyobj.arr[0].name = 'arr2';
copyobj.func = () => 'sourcefunc2';
copyobj.obj.string = 'obj2';
copyobj.obj.func = () => 'objfunc2';
// 执行
console.log(sourceobj);
/* {
number: 1,
string: 'source1',
boolean: true,
null: null,
undefined: undefined,
arr: [{name: 'arr2'}],
func: () => 'sourcefunc1',
obj: {
func: () => 'objfunc2',
string: 'obj2'
}
}
*/
2. 深拷贝
深拷贝能够实现真正意义上的对象的拷贝,实现方法就是递归调用“浅拷贝”。深拷贝会创造一个一模一样的对象,其内容地址是自助分配的,拷贝结束之后,内存中的值是完全相同的,但是内存地址是不一样的,目标对象跟源对象不共享内存,修改任何一方的值,不会对另外一方造成影响。
/* sourceobj 表示源对象
* 执行完函数,返回目标对象
*/
function deepclone (sourceobj = {}) {
let targetobj = array.isarray(sourceobj) [] : {};
let copy;
for (var key in sourceobj) {
copy = sourceobj[key];
if (typeof(copy) === 'object') {
if (copy instanceof object) {
targetobj[key] = deepclone(copy);
} else {
targetobj[key] = copy;
}
} else if (typeof(copy) === 'function') {
targetobj[key] = eval(copy.tostring());
} else {
targetobj[key] = copy;
}
}
return targetobj;
}
// 定义 sourceobj
let sourceobj = {
number: 1,
string: 'source1',
boolean: true,
null: null,
undefined: undefined,
arr: [{name: 'arr1'}],
func: () => 'sourcefunc1',
obj: {
string: 'obj1',
func: () => 'objfunc1'
}
}
// 拷贝sourceobj
let copyobj = deepclone(sourceobj);
// 修改 source
copyobj.number = 2;
copyobj.string = 'source2';
copyobj.boolean = false;
copyobj.arr[0].name = 'arr2';
copyobj.func = () => 'sourcefunc2';
copyobj.obj.string = 'obj2';
copyobj.obj.func = () => 'objfunc2';
// 执行
console.log(sourceobj);
/* {
number: 1,
string: 'source1',
boolean: true,
null: null,
undefined: undefined,
arr: [{name: 'arr1'}],
func: () => 'sourcefunc1',
obj: {
func: () => 'objfunc1',
string: 'obj1'
}
}
*/
两个方法可以合并在一起:
/* deep 为 true 表示深复制,为 false 表示浅复制
* sourceobj 表示源对象
* 执行完函数,返回目标对象
*/
function clone (deep = true, sourceobj = {}) {
let targetobj = array.isarray(sourceobj) [] : {};
let copy;
for (var key in sourceobj) {
copy = sourceobj[key];
if (deep && typeof(copy) === 'object') {
if (copy instanceof object) {
targetobj[key] = clone(deep, copy);
} else {
targetobj[key] = copy;
}
} else if (deep && typeof(copy) === 'function') {
targetobj[key] = eval(copy.tostring());
} else {
targetobj[key] = copy;
}
}
return targetobj;
}
使用技巧
1. concat()、slice()
(1)若拷贝数组是纯数据(不含对象),可以通过concat() 和 slice() 来实现深拷贝;
let a = [1, 2];
let b = [3, 4];
let copy = a.concat(b);
a[1] = 5;
b[1] = 6;
console.log(copy);
// [1, 2, 3, 4]
let a = [1, 2];
let copy = a.slice();
copy[0] = 3;
console.log(a);
// [1, 2]
(2)若拷贝数组中有对象,可以使用 concat() 和 slice() 方法来实现数组的浅拷贝。
let a = [1, {name: 'hh1'}];
let b = [2, {name: 'kk1'}];
let copy = a.concat(b);
copy[1].name = 'hh2';
copy[3].name = 'kk2';
console.log(copy);
// [1, {name: 'hh2'}, 2, {name: 'kk2'}]
无论 a[1].name 或者 b[1].name 改变,copy[1].name 的值都会改变。
let a = [1, {name: 'hh1'}];
let copy = a.slice();
copy[1].name = 'hh2';
console.log(a);
// [1, {name: 'hh2'}]
改变了 a[1].name 后,copy[1].name 的值也改变了。
2. object.assign()、object.create()
object.assign()、object.create() 都是一层(根级)深拷贝,之下的级别为浅拷贝。
(1) 若拷贝对象只有一级,可以通过 object.assign()、object.create() 来实现对象的深拷贝;
let sourceobj = {
str: 'hh1',
number: 10
}
let targetobj = object.assign({}, sourceobj)
targetobj.str = 'hh2'
console.log(sourceobj);
// {str: 'hh1', number: 10}
let sourceobj = {
str: 'hh1',
number: 10
}
let targetobj = object.create(sourceobj)
targetobj.str = 'hh2'
console.log(sourceobj);
// {str: 'hh1', number: 10}
(2) 若拷贝对象有多级, object.assign()、object.create() 实现的是对象的浅拷贝。
let sourceobj = {
str: 'hh',
number: 10,
obj: {
str: 'kk1'
}
}
let targetobj = object.assign({}, sourceobj)
targetobj.obj.str = 'kk2'
console.log(sourceobj);
// {
// str: 'hh',
// number: 10,
// obj: {
// str: 'kk2'
// }
// }
let sourceobj = {
str: 'hh',
number: 10,
obj: {
str: 'kk1'
}
}
let targetobj = object.create(sourceobj)
targetobj.obj.str = 'kk2'
console.log(sourceobj);
// {
// str: 'hh',
// number: 10,
// obj: {
// str: 'kk2'
// }
// }
修改了 targetobj.obj.str 的值之后,sourceobj.obj.str 的值也改变了。
3. 对象的解构
对象的解构同 object.assign() 和 object.create(),都是一层(根级)深拷贝,之下的级别为浅拷贝。
(1)若拷贝对象只有一层,可以通过对象的解构来实现深拷贝;
let sourceobj = {
str: 'hh1',
number: 10
}
let targetobj = {...sourceobj};
targetobj.str = 'hh2'
console.log(sourceobj);
// {str: 'hh1', number: 10}
(2)若拷贝对象有多层,通过对象的解构实现的是对象的浅拷贝。
let sourceobj = {
str: 'hh',
number: 10,
obj: {
str: 'kk1'
}
}
let targetobj = {...sourceobj};
targetobj.obj.str = 'kk2'
console.log(sourceobj);
// {
// str: 'hh',
// number: 10,
// obj: {
// str: 'kk2'
// }
// }
4. json.parse()
用 json.stringify() 把对象转成字符串,再用 json.parse() 把字符串转成新的对象,可以实现对象的深复制。
let source = ['hh', 1, [2, 3], {name: 'kk1'}];
let copy = json.parse(json.stringify(source));
copy[2][1] = 4;
copy[3].name = 'kk2';
console.log(source);
// ['hh', 1, [2, 3], {name: 'kk1'}]
可以看出,虽然改变了 copy[2].name 的值,但是 source[2].name 的值没有改变。
json.parse(json.stringify(obj)) 不仅能复制数组还可以复制对象,但是几个弊端:
1)它会抛弃对象的 constructor,深拷贝之后,不管这个对象原来的构造函数是什么,在深拷贝之后都会变成 object;
2)这种方法能正确处理的对象只有number, string, boolean, array, 扁平对象,即那些能够被 json 直接表示的数据结构。regexp 对象是无法通过这种方式深拷贝。
3)只有可以转成 json 格式的对象才可以这样用,像 function 没办法转成 json。
5. 可以使用的库
上一篇: 一种简单又实用的书法字体设计方法
下一篇: CAD2014怎么使用设计中心功能?