深拷贝与浅拷贝
关于这个知识点的话,网上的总结已经非常多了,但是黑鸡一直没有去总结,今天看了自己的笔记里没有这个总结,所以打算对这个知识写一篇笔记。
值类型与引用类型
说到深拷贝与浅拷贝,就必然离不开值类型和引用类型,JavaScript的数据类型有下面几种
| 值类型 | 引用类型 |
|---|---|
字符串string、数字number、布尔值boolean、空null、为定义undefined、符号symbol | 数组Array、对象Object |
二者的区别在于数据的储存方式不一样。
值类型的值是储存在栈中,我们赋值的话会直接修改栈中的值,所以不存在深拷贝浅拷贝的问题。
引用类型的值是储存值堆中,栈中储存的是堆中的内存地址,所以我们中赋值给其他变量的时候赋的值其实是栈中的内存地址,这时候两个变量会指向堆中的同一内存地址,所以会互相干扰,所以才会有深拷贝与浅拷贝的说法,举个例子:
let obj = { name: "dabanheiji", age: 23 };
let newObj = obj; //赋的值是内存地址
// 这个时候obj和newObj指向同一内存地址,其实是同一个对象
obj.age = 18
console.log(newObj) // { name: "dabanheiji", age: 18 }
从上面的例子可以看出来我们没有修改newObj但是它却改变了,不过有的时候我们不希望这样,下面总结了几种方法,并且说明了适用场景
常用的拷贝方法
解构赋值
使用解构的赋值方式会中堆中开辟一块新的内存,从而让变量指向不同的内存地址,但是需要注意:解构只能中对象中不存在引用类型的值的时候使用才算是深拷贝
let obj = { name: "大阪黑鸡", age: 23 };
let newObj = {...obj};
obj.name = "滚筒洗衣机";
console.log(obj) // { name: "滚筒洗衣机", age: 23 }
console.log(newObj) // { name: "大阪黑鸡", age: 23 }
/** ------------------------------------------ */
let origin = {
name: "大阪黑鸡",
hobby: [
"吃饭",
"睡觉"
]
}
let newOrigin = {...origin}
newOrigin.name = "滚筒洗衣机"
newOrigin.hobby.push("打豆豆")
console.log(origin)
/**
{
name: '大阪黑鸡',
hobby: [ '吃饭', '睡觉', '打豆豆' ]
}
*/
console.log(newOrigin)
/**
{
name: '滚筒洗衣机',
hobby: [ '吃饭', '睡觉', '打豆豆' ]
}
*/
从上面的例子可以看出来,解构赋值确实会开辟新的内存地址,但是对于对象内部使用的引用类型数据还是会存在两个变量指向同一内存地址的情况,所以解构赋值在对象中没有引用类型的情况下可以使用做为快捷方法,但是严格来说解构赋值还是浅拷贝
Object.assign
这个方法与解构赋值的作用差不多,它的作用是可以将多个对象合并成一个对象,合并后的对象内存地址是第一个参数的内存地址,所以第一个参数传一个空对象的情况下与解构赋值效果一致
let obj = { name: "大阪黑鸡", age: 23 };
let newObj = Object.assign({}, obj);
obj.name = "滚筒洗衣机";
console.log(obj) // { name: "滚筒洗衣机", age: 23 }
console.log(newObj) // { name: "大阪黑鸡", age: 23 }
/** ------------------------------------------ */
let origin = {
name: "大阪黑鸡",
hobby: [
"吃饭",
"睡觉"
]
}
let newOrigin = Object.assign({}, origin)
newOrigin.name = "滚筒洗衣机"
newOrigin.hobby.push("打豆豆")
console.log(origin)
/**
{
name: '大阪黑鸡',
hobby: [ '吃饭', '睡觉', '打豆豆' ]
}
*/
console.log(newOrigin)
/**
{
name: '滚筒洗衣机',
hobby: [ '吃饭', '睡觉', '打豆豆' ]
}
*/
从上面例子可用看出此方法严格来说也是浅拷贝。
JSON.parse(JSON.stringify())
上面介绍的两种浅拷贝方法在特定的情况下(对象的值全是基本数据类型)可以实现与深拷贝一样的效果,接下来说的JSON.parse(JSON.stringify())则是真正的深拷贝
let origin = {
name: "大阪黑鸡",
hobby: ["吃饭", "睡觉"]
}
let newOrigin = JSON.parse(JSON.stringify(origin))
newOrigin.name = "滚筒洗衣机"
newOrigin.hobby.push("打豆豆")
console.log(origin)
/**
{
name: '大阪黑鸡',
hobby: [ '吃饭', '睡觉' ]
}
*/
console.log(newOrigin)
/**
{
name: '滚筒洗衣机',
hobby: [ '吃饭', '睡觉', '打豆豆' ]
}
*/
此方式可以实现深拷贝,适用与大部分场景,当然也是有缺点的,那就是会跳过undefined和函数方法,所以只是适用大部分场景而不是全部场景,比如
let origin = {
name: "大阪黑鸡",
hobby: ["吃饭", "睡觉"],
skill: undefined,
sayHello(){
console.log("hello")
}
}
let newOrigin = JSON.parse(JSON.stringify(origin))
newOrigin.name = "滚筒洗衣机"
newOrigin.hobby.push("打豆豆")
console.log(origin)
/**
{
name: '大阪黑鸡',
hobby: [ '吃饭', '睡觉' ],
skill: undefined,
sayHello: [Function: sayHello]
}
*/
console.log(newOrigin)
/**
{
name: '滚筒洗衣机',
hobby: [ '吃饭', '睡觉', '打豆豆' ]
}
无法拷贝undefined和函数方法
*/
递归拷贝
上面的拷贝方法已经可以覆盖绝大部分场景,但是都无法做到真正完美的深拷贝,想实现完美的深拷贝只能自己写一个递归来实现
let origin = {
name: "大阪黑鸡",
hobby: ["吃饭", "睡觉"],
skill: undefined,
sayHello(){
console.log("hello")
},
empty: null,
num: 100,
obj: {
a: {
b: {
c: "c"
}
}
},
time: new Date(),
reg: /\.js/g
}
let newOrigin = deepClone(origin)
newOrigin.name = "滚筒洗衣机"
newOrigin.hobby.push("打豆豆")
console.log(origin)
/**
{
name: '大阪黑鸡',
hobby: [ '吃饭', '睡觉' ],
skill: undefined,
sayHello: [Function: sayHello],
empty: null,
num: 100,
obj: { a: { b: { c: "c" } } },
time: 2021-08-10T07:11:27.683Z,
reg: /\.js/g
}
*/
console.log(newOrigin)
/**
{
name: '滚筒洗衣机',
hobby: [ '吃饭', '睡觉', '打豆豆' ],
skill: undefined,
sayHello: [Function: sayHello],
empty: null,
num: 100,
obj: { a: { b: { c: "c" } } },
time: 2021-08-10T07:11:27.683Z,
reg: /\.js/g
}
*/
function deepClone(origin){
if(typeof origin !== 'object') return origin;
if(origin === null) return origin;
if(origin instanceof Date) return new Date(origin);
if(origin instanceof RegExp) return new RegExp(origin);
let result = origin.constructor();
for(let k in origin){
result[k] = deepClone(origin[k])
}
return result
}