Javascript中的对象拷贝(对象复制/克隆)
Jack Lee 的 CSDN 博客
邮箱 :291148484@163.com
CSDN 主页:https://blog.csdn.net/qq_28550263?spm=1001.2101.3001.5343
本文地址:https://blog.csdn.net/qq_28550263/article/details/117751704
提示:
可以直接使用工具模块
@jcstdio/jc-utils
的copy
进行深拷贝或浅拷贝。安装:
npm install @jcstdio/jc-utils
使用:
import { copy } from '@jcstdio/jc-utils' // 浅拷贝 const obj1 = {a: 1, b: {c: 2}}; const shallowCopyObj1 = copy.shallowCopy(obj1); console.log(shallowCopyObj1); // {a: 1, b: {c: 2}} console.log(shallowCopyObj1.b === obj1.b); // true // 使用 deepCopy 方法深拷贝 (递归复制嵌套对象实现) const obj2 = {a: 1, b: {c: 2}}; const deepCopyObj2 = copy.deepCopy(obj2); console.log(deepCopyObj2); // {a: 1, b: {c: 2}} console.log(deepCopyObj2.b === obj2.b); // false // 使用 serializeCopy 方法深拷贝 (使用JSON序列化和反序列化实现) const obj3 = {a: 1, b: {c: 2}}; const serializeCopyObj3 = copy.serializeCopy(obj3); console.log(serializeCopyObj3); // {a: 1, b: {c: 2}} console.log(serializeCopyObj3.b === obj3.b); // false
其中:
@jcstdio/jc-utils
是一个用于 NodeJS、浏览器 中的编程工具模块。可参考博文 《JavaScript/TypeScript 编程工具集-@jcstdio/jc-utils
模块》 了解更多。
目 录
1. 对象的引用
要说“拷贝”还要先说“引用”的概念。
在JavaScript中没有“指针”的概念,但保留了对象的“引用”。首先必须明确,与“Java”、“Python”等经典面向对象编程语言中“一切皆可对象”不同,在JavaScript中绝非一切皆是对象,并且“引用”是“对象”的引用。这里就需要区分在赋值操作“=”的右侧,到底是一个“字面量”还是一个对象。举例而言:
vara=1; varb="Hello World!"varc=a
这里的数字1
和"Hello World!
"都并非对象,而是不可变的字面量值。他们分别源于简单基本类型number
和string
而不是内置对象Number
和String
。(不过,对于左侧的变量,如果你要使用一些方法或属性,如.leng
,并不需要显式将string转为String对象)
由于右侧不是对象,这在赋值操作var c = a
传递的是值,即将number 1传给变量c,换成var c = b
也类似。
而下例中:
vard= { Hello : 'world'}; vare=d;
由于变量d
的右侧是一个变量,这时var d = {Hello : 'world'};
使得变量d
创建了一个引用,即使得d中保存了右侧那个对象的地址,我们可以对变量d
间接对右侧那个变量进行操作,如d.Hello
。而后一个赋值操作var e = d;
传递的是引用的内容,由于第一个赋值使得d
引用到了其右侧那个对象的内存地址,后一个赋值只不过是将变量d
中所存储的内容地址赋值了一份给变量e
而已,因此这时变量e
与d
引用到了同一个对象。
2. 浅拷贝
浅拷贝,又称“浅层拷贝”、“浅复制”、“浅层复制”等等。
一个很常见的通俗说法,“浅层拷贝就是只拷贝一层”,这样的说法其实不太准确。
浅拷贝的本质特点是:
- 拷贝原对象后将得到一个新对象;
- 新对象的将中的所有
属性
都是原对象中对应属性的一个引用。
因此从表象上看,浅拷贝拷贝出来的新对象中所有属性的值会复制原对象中对应属性的值。例如:
varobj_a= { is_right : true} varobj_b== { a : 1, b : obj_a}
如果获取对象obj_b
的浅拷贝得到一个新对象,可以使用ES6提供的Object.assign()
方法:
var new_obj = Object.assign({}, obj_b);
其中Object.assign()
方法,第一个参数是目标对象,后面为一个或多个源对象。
该方法对obj_b
实现浅拷贝的过程为,遍历一个或多个源对象(从第二个参数开始)所有可枚举的自有键,并逐个进行“=”完成复制,最后返回目标对象。
上例中,对于键值对“a:1
”,由于数字“1
”是一个不可变的值(字面量)而非数字,new_obj
的第一个键值对完全相同,故有:
new_obj.a==1// true
然而第二个键值对的值obj_a
引用了一个对象,进行“=
”复制到new_obj
的键值时,传递的时引用的地址,这样使得new_obj
对应与键b
的值引用到了对象obj_a
。即必有:
new_obj.b===obj_a
那么,为什么称浅层
拷贝呢?
以上我们已经了解了这种拷贝方法的本质,但是如果想要了解这个名字的由来,我们不妨假设上例中,将obj_a
修改为一个键值中含有对象的对象。如:
varobj_c= { are_you_ok : true} varobj_a= { is_right : true, is_ok : obj_c} varobj_b== { a : 1, b : obj_a}
这时对 obj_b
进行浅拷贝得到new_obj
后,new_obj.is_ok
对应的值为由obj_a.is_ok
对应的值obj_c
。由于变量obj_c
中存储的是对一个对象的引用,这里传递的同样是被引用对象的地址,但地址中所存储的呢欧容不会进一步拷贝,这样在new_obj
中,并不会存储一个
{are_you_ok : true}
这样的实际对象。因此看起来我们“只拷贝了表层”。这就是所谓“浅”拷贝称为的由来。
这里最后还有一个需要注意的问题,使用Object.assign()
方法浅拷贝时由于相当于只是对源对象的所有可枚举的自有键一一进行“=”赋值操作,对于由属性描述符所描述的一些属性的特性是不会被拷贝到目标对象的。如某个属性是否可修改(Writeable)、可配置(Configurable)、可枚举(Enumerable)。
3. 深拷贝
在JavaScript中,深拷贝说起来有点麻烦,因为里面情况会很比较复杂。
相比于浅拷贝,深拷贝要求要完整地拷贝下底层被引用地对象而不是仅粗略地要求拷贝下引用中对象的地址。正如之前所说通俗一点理解看:
- 仅用"="将对象引用的赋值给变量时,仅传递了引用的对象地址;
- 通过“浅拷贝”返回对象时,相当于对源对象最外层的所有可枚举属性进行了“=”操作后获得的新对象;
- 通过“深拷贝”返回对象时,相当于任意一层不再简单传递引用的对象的内存地址,而是真正意义上拷贝下来每一层对象。
但是很快你就会发现,如果在一个对象中引用的对象到某层由存在循环性的引用,往往会导致一个死循环。
另外,在JavaScript中的函数也是对象,我们在JS中不能确定对一个函数进行“深拷贝”是什么,尽管由很多框架给出了自己的定义。
一种比较好方法是通过JSON序列化来实现深拷贝:
varobj_b=JSON.parse(JSON.stringify(obj_a))
这种方法也不是万能的,它要求对象必须是JSON安全
的,即:
不仅obj_a
可以被序列化为一个JSON格式的字符串,同时还可以由该字符串解析得到一个结构
与值
完全相同的对象。