浅拷贝和深拷贝的区别
大家都以为浅拷贝就是把引用类型的值拷贝一份,实际还是引用的同一个对象,把这个叫浅拷贝,实际上这是一个大大的误会。
- 浅拷贝和深拷贝都复制了值和地址,都是为了解决引用类型赋值后相互影响的问题。
- 但是浅拷贝只进行一层复制,深层次的引用类型还是共享内存地址。源对象和拷贝对象还是会相互影响
- 深拷贝就是无限层级拷贝,深拷贝后的原对象不会和和拷贝对象互相影响。
实现浅拷贝的方式有哪些呢?
- Object.assign
- 数组的slice和concat方法
- 数组静态方法Array.from
- 扩展运算符
实现深拷贝
要求:
- 支持对象、数组、日期、正则的拷贝。
- 处理Map 和 Set
- 处理原始类型(原始类型直接返回,只有引用类型才有深拷贝这个概念)。
- 处理 Symbol 作为键名的情况。
- 处理函数(函数直接返回,拷贝函数没有意义,两个对象使用内存中同一个地址的函数,没有任何问题)。
- 处理 DOM 元素(DOM 元素直接返回,拷贝 DOM 元素没有意义,都是指向页面中同一个)。
- 额外开辟一个储存空间 WeakMap,解决循环引用递归爆栈问题(引入 WeakMap 的另一个意义,配合垃圾回收机制,防止内存泄漏)。
我们要实现的目标:
const target = {
a: true,
b: 100,
c: 'str',
d: undefined,
e: null,
f:new Date(),
g: /abc/,
h:{
a:'ccc',
b:12
},
i:[1,2,3,4],
j:Symbol("age"),
k:new Set([1,2,3,4]),
l:new Map([[1,2],[3,4]]),
[nameSymbol]:"job",
};
target.target = target;
1.最简单的实现,不考虑引用类型
要拷贝的对象
const target = {
a: true,
b: 100,
c: 'str',
d: undefined,
};
要拷贝这个对象,我们只需要把对象里面的数据按个拷贝出来就可以了。
function deepClone(target){
let cloneTarget = {};
for(const key in target){
cloneTarget[key] = target[key];
}
return cloneTarget;
}
2.判断处理 Null的情况
const target = {
a: true,
b: 100,
c: 'str',
d: undefined,
e: null,
};
我们只需要添加一个判断就可以了。
function deepClone(target){
//如果是null 就返回
if(target === null){
return target;
}
let cloneTarget = {};
for(const key in target){
cloneTarget[key] = target[key];
}
return cloneTarget;
}
3.判断处理日期的情况
const target = {
a: true,
b: 100,
c: 'str',
d: undefined,
e: null,
f:new Date(),
};
日期是一个对象,我们可以通过日期的构造函数重新new一个对象来进行复制
function deepClone(target){
//如果是null 就返回
if(target === null){
return target;
}
if(target instanceof Date){
return new Date(target);
}
let cloneTarget = {};
for(const key in target){
cloneTarget[key] = target[key];
}
return cloneTarget;
}
4.判断处理正则的情况
const target = {
a: true,
b: 100,
c: 'str',
d: undefined,
e: null,
f:new Date(),
g: /abc/,
};
正则和日期一样,同样也可以使用构造函数来处理。
function deepClone(target){
//如果是null 就返回
if(target === null){
return target;
}
if(target instanceof Date){
return new Date(target);
}
if(target instanceof RegExp){
return new RegExp(target);
}
let cloneTarget = {};
for(const key in target){
cloneTarget[key] = target[key];
}
return cloneTarget;
}
5.深拷贝复制引用类型
const target = {
a: true,
b: 100,
c: 'str',
d: undefined,
e: null,
f:new Date(),
g: /abc/,
h:{
a:'ccc',
b:12
}
};
里面既然有了引用类型,那么我们只需要递归调用一下,然后返回就可以了。
arguments.callee
这里指向函数的引用。
function deepClone(target){
//如果是null 就返回
if(target === null){
return target;
}
if(target instanceof Date){
return new Date(target);
}
if(target instanceof RegExp){
return new RegExp(target);
}
//处理引用类型 以免死循环
if(typeof target !== 'object'){
return target;
}
const cloneTarget = {}
for(const key in target){
cloneTarget[key] = deepClone(target[key]);
}
return cloneTarget;
}
6.处理数组的情况
const target = {
a: true,
b: 100,
c: 'str',
d: undefined,
e: null,
f:new Date(),
g: /abc/,
h:{
a:'ccc',
b:12
},
i:[1,2,3,4]
};
数组这了比较简单,它的区别仅仅只是我们拷贝的是对象还是数组。
function deepClone(target){
//如果是null 就返回
if(target === null){
return target;
}
if(target instanceof Date){
return new Date(target);
}
if(target instanceof RegExp){
return new RegExp(target);
}
//处理引用类型 以免死循环
if(typeof target !== 'object'){
return target;
}
// 处理对象和数组 以及原型链
const cloneTarget = new target.constructor() // 创建一个新的克隆对象或克隆数组
for(const key in target){
cloneTarget[key] = deepClone(target[key]);
}
return cloneTarget;
}
可以看到上面有一个骚操作,就是我们通过实例的constructor
拿到它的构造函数,然后直接new
就可以了。
这样就不用在去判断是否是数组还是对象,然后去调用它对应的构造函数,当然这里这样写有一定的风险。如果作为底层库来使用,需要考虑constructor
并没有指向它构造函数的情况。
不过通过它的构造函数我们解决了另外一个问题,就是原型链。通过它原本的构造函数,原型链自然也是完整保存的。意想不到的小细节
7.处理Symbol的情况
const target = {
a: true,
b: 100,
c: 'str',
d: undefined,
e: null,
f:new Date(),
g: /abc/,
h:{
a:'ccc',
b:12
},
i:[1,2,3,4],
j:Symbol("age"),
[Symbol("name")]:"job",
};
Symbol
的特性就是全局唯一值,里面的参数只是一个描述符。而并不是具体值。这里在处理的时候,我们需要考虑两种情况。
- 一种是作为值存在的
Symbol
- 第二种是作为键存在的
Symbol
作为值存在的话,我们可以通过Symbol.prototype.toString
方法拿到它的描述字段。然后重新构造一个Symbol
//处理值为Symbol的情况
if(typeof target === 'symbol'){
return Symbol(target.toString());
}
作为键存在的话,for in
的遍历范围就无法满足我们的要求的,所以需要换成遍历范围更加合适的Reflect.ownKeys()
。
下面是MDN的原话
Reflect.ownKeys 方法返回一个由目标对象自身的属性键组成的数组。
它的返回值等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))。
同时使用Reflect.ownKeys
解决了for in
的一个隐藏的问题。那就是会把原型对象的属性也遍历下来,然后存储到拷贝对象里面。
升级后的效果:
function deepClone(target,map = new WeakMap()){
//如果是null 就返回
if(target === null){
return target;
}
//处理日期
if(target instanceof Date){
return new Date(target);
}
//处理正则表达式
if(target instanceof RegExp){
return new RegExp(target);
}
//处理值为Symbol的情况
if(typeof target === 'symbol'){
return Symbol(target.toString());
}
//处理引用类型 以免死循环
if(typeof target !== 'object'){
return target;
}
// 处理对象和数组
const cloneTarget = new target.constructor() // 创建一个新的克隆对象或克隆数组
//通过Reflect来拿到所有可枚举和不可枚举的属性,以及Symbol的属性。
Reflect.ownKeys(target).forEach(key=>{
cloneTarget[key] = deepClone(target[key]);
})
return cloneTarget;
}
8.处理循环引用的情况
const target = {
a: true,
b: 100,
...
};
target.target = target;
数据是上面的这样。这种的数据会让递归进入死循环从而造成爆栈的问题。
这里可以通过WeakMap
来建立当前对象和拷贝对象的弱引用关系,判断当前对象是否存在,如果存在就使用它
保存的对象,如果不存在就添加进去。
WeakMap
的原理是,它的键值只能是引用类型,它的这个引用类型并不会强制标记,当垃圾回收机制需要释放内存的时候,它会被直接释放,而不需要做其他的操作,使用者也不担心内存泄漏的问题。
我们用WeakMap
改造升级一下。
function deepClone(target,map = new WeakMap()){
//如果是null 就返回
if(target === null){
return target;
}
//处理日期
if(target instanceof Date){
return new Date(target);
}
//处理正则表达式
if(target instanceof RegExp){
return new RegExp(target);
}
//处理值为Symbol的情况
if(typeof target === 'symbol'){
return Symbol(target.toString());
}
//处理引用类型 以免死循环
if(typeof target !== 'object'){
return target;
}
//判断释放存在
if(map.has(target)){
return target;
}
// 处理对象和数组
const cloneTarget = new target.constructor() // 创建一个新的克隆对象或克隆数组
//保存原引用和拷贝引用的关系
map.set(target,cloneTarget)
//通过Reflect来拿到所有可枚举和不可枚举的属性,以及Symbol的属性。
Reflect.ownKeys(target).forEach(key=>{
cloneTarget[key] = deepClone(target[key],map);
})
return cloneTarget;
}
这样就可以处理循环引用的问题了。
9.处理 Set和Map的情况 和HTMLElement的情况
因为这两个都是可迭代的数据结构,同时它们又有自己的添加属性的方法。所以需要按个判断。
由于之前 const cloneTarget = new target.constructor()
,我们并不需要去手动添加处理Map
和Set
。
而HTMLElement的情况并不需要处理,拷贝也没有意义,直接返回就好。
function deepClone(target,map = new WeakMap()){
//如果是null 就返回
if(target === null) return target;
//处理日期
if(target instanceof Date) return new Date(target);
//处理正则表达式
if(target instanceof RegExp) return new RegExp(target);
//处理值为Symbol的情况
if(typeof target === 'symbol') return Symbol(target.toString());
// 处理 DOM元素
if (target instanceof HTMLElement) return target
//非引用类型的直接返回 比如函数 就不需要处理,直接返回就好
if(typeof target !== 'object') return target;
//从缓冲中读取
if(map.has(target)) return target;
// 处理对象和数组
const cloneTarget = new target.constructor() // 创建一个新的克隆对象或克隆数组
//保存原引用和拷贝引用的关系
map.set(target,cloneTarget)
//处理Map的情况
if(target instanceof Map){
for(let [key,value] of target){
target.set(key,deepClone(value,map));
}
return target;
}
//处理set的情况
if(target instanceof Set){
for(let value of target){
target.add(deepClone(value,map))
}
return target;
}
//通过Reflect来拿到所有可枚举和不可枚举的属性,以及Symbol的属性。
Reflect.ownKeys(target).forEach(key=>{
cloneTarget[key] = deepClone(target[key],map);
})
return cloneTarget;
}
差不多就是这样了。我们处理了下面的情况:
const target = {
a: true,
b: 100,
c: 'str',
d: undefined,
e: null,
f:new Date(),
g: /abc/,
h:{
a:'ccc',
b:12
},
i:[1,2,3,4],
j:Symbol("age"),
k:new Set([1,2,3,4]),
l:new Map([[1,2],[3,4]]),
[nameSymbol]:"job",
};
target.target = target;
打印结果:
{
a: true,
b: 100,
c: 'str',
d: undefined,
e: null,
f: 2022-03-30T09:47:13.762Z,
g: /abc/,
h: { a: 'ccc', b: 12 },
i: [ 1, 2, 3, 4 ],
j: Symbol(Symbol(age)),
k: Set(4) { 1, 2, 3, 4 },
l: Map(2) { 1 => 2, 3 => 4 },
target: <ref *1> {
a: true,
b: 100,
//这里省略折起...
},
[Symbol(name)]: 'job'
}
如果真的要实现一个深拷贝 ,那么情况要复杂的多,可以参考lodash
的源码学习。
聊一下另外一个深拷贝的方式: JSON.parse(JSON.stringify(target))
我们使用这个来实现深拷贝 ,直接深拷贝上面的测试用例,看看能够有几个?
const target = {
0:NaN,
1:Infinity,
2:-Infinity,
a: true,
b: 100,
c: 'str',
d: undefined,
e: null,
f:new Date(),
g: /abc/,
h:{
a:'ccc',
b:12
},
i:[1,2,3,4],
j:Symbol("age"),
k:new Set([1,2,3,4]),
l:new Map([[1,2],[3,4]]),
[nameSymbol]:"job",
};
target.target = target;
JSON.parse(JSON.stringify(target))
//报错:
VM8398:1 Uncaught TypeError: Converting circular structure to JSON
--> starting at object with constructor 'Object'
--- property 'target' closes the circle
at JSON.stringify (<anonymous>)
at <anonymous>:1:17
意思循环引用错误。我们把循环引用的代码去掉:
target.target = target;
看一下打印出来的结果:
'0': null,
'1': null,
'2': null,
a: true
b: 100
c: "str"
e: null
f: "2022-03-30T11:20:08.362Z"
g: {}
h: {a: 'ccc', b: 12}
i: (4) [1, 2, 3, 4]
k: {}
l: {}
[[Prototype]]: Object
从头看到尾:
-
d: undefined,
没有了,无法处理值 为undefined
的情况 -
f:2022-03-30T11:20:08.362Z
时间变成了字符串 -
g: {}
正则表达式 没有了 -
j:Symbol("age")
所有Symbol的值都没有了, -
k:new Set([1,2,3,4])
Set 没有了 -
l:new Map([[1,2],[3,4]])
Map 没有了
会忽略的有 : undefined
,Symbol
,函数
,直接不存在
会变成对象:Map
,Set
,正则表达式
会被序列化为Null:NaN
、Infinity
、-Infinity
不能循环引用。
学习参考的文章:感谢这些大佬,我才能站在巨人的肩膀上。
轻松拿下 JS 浅拷贝、深拷贝
如何写出一个惊艳面试官的深拷贝?
网友评论