浅拷贝与深拷贝

本文最后更新于:1 年前

浅拷贝与深拷贝

在了解深浅拷贝之前应该了解JavaScript的数据类型

数据类型

数据类型分为基本数据类型(String, Number, Boolean, Null, Undefined, Symbo, Symbol (ES6), BigInt (ES2020))和引用数据类型 (Object、Array、Function)。

基本数据类型的特点:直接存储在栈(stack)中的数据
引用数据类型的特点:存储的是该对象在栈中引用,真实的数据存放在堆内存里

引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。
当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
image.png

深拷贝和浅拷贝的定义

深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的。

  • 浅拷贝是创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象
  • 深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

浅拷贝:image.png

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。

深拷贝:
image.png

深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

赋值

当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据
赋值是将某一数值或对象赋给某个变量的过程,分为下面 2 部分

  • 基本数据类型:赋值,赋值之后两个变量互不影响
  • 引用数据类型:赋,两个变量具有相同的引用,指向同一个对象,相互之间有影响

对基本类型进行赋值操作,两个变量互不影响。

1
2
3
4
5
6
7
let a = "muyiy";
let b = a;
console.log(b);// muyiy

a = "change";
console.log(a);// change
console.log(b);// muyiy

对引用类型进行赋操作,两个对象指向的同一个内存地址,修改引用值会对另一个影响

1
2
3
4
5
6
7
8
9
10
11
12
let person1 = {
name: '张三',
age: 18,
sex: 'male',
height: 180,
weight: 140,
}
let person2 = person1
person2.name = '李四'
console.log(person1, person2)
// {name: '李四', age: 18, sex: 'male', height: 180, weight: 140}
// {name: '李四', age: 18, sex: 'male', height: 180, weight: 140}

浅拷贝

浅拷贝会重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。
简单来说可以理解为浅拷贝只解决了第一层的问题,拷贝第一层的基本类型值,以及第一层的引用类型地址
遍历对象,将每一个属性和值分别赋值给一个空对象。但是浅拷贝没有处理引用值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let person1 = {
name: '张三',
age: 18,
sex: 'male',
height: 180,
weight: 140,
}
let person2 = {}
for (let key in person1) {
person2[key] = person1[key]
}
person2.name = '李四'
console.log(person1, person2)
// {name: '张三', age: 18, sex: 'male', height: 180, weight: 140}
// {name: '李四', age: 18, sex: 'male', height: 180, weight: 140}

浅拷贝的问题

  • 没有处理引用值,引用值还是指向同一个地址。
  • 如果原型上还有其他属性,浅拷贝也会拷贝下来
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    Object.prototype.num = 1
    let person1 = {
    name: '张三',
    age: 18,
    sex: 'male',
    height: 180,
    weight: 140,
    son: {
    first: 'Jenney',
    },
    }
    let person2 = {}
    //浅拷贝
    for (let key in person1) {
    person2[key] = person1[key]
    }
    person2.name = '李四'
    person2.son.second = 'Lucy'
    console.log(person1, person2)
    // age: 18
    // height: 180
    // name: "张三"
    // sex: "male"
    // son: {first: 'Jenney', second: 'Lucy'}
    // weight: 140
    // [[Prototype]]: Object
    // num: 1

    // age: 18
    // height: 180
    // name: "李四"
    // num: 1
    // sex: "male"
    // son: {first: 'Jenney', second: 'Lucy'}
    // weight: 140
    // [[Prototype]]: Object
    // num: 1

    实现浅拷贝

    用 for in 实现浅拷贝

    传入拷贝源和目标对象,考虑排除原型上的对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    Object.prototype.num = 1
    let person1 = {
    name: '张三',
    age: 18,
    sex: 'male',
    height: 180,
    weight: 140,
    son: {
    first: 'Jenney',
    },
    }
    //浅拷贝
    let person2 = clone(person1)
    person2.name = '李四'
    person2.son.second = 'Lucy'
    console.log(person1, person2)

    //浅拷贝
    function clone(origin, target) {
    let tar = target || {}
    for (let key in origin)
    if (origin.hasOwnProperty(key)) {//排除原型,只打印自身属性
    tar[key] = origin[key]
    }
    return tar
    }

    Object.assign()(推荐)

    for in 方法做浅拷贝过于繁琐。ES6 给我们提供了新的语法糖,通过 Object.assgin() 可以实现浅拷贝
    1
    2
    3
    4
    5
    // 语法1
    obj2 = Object.assgin(obj2, obj1);

    // 语法2
    Object.assign(目标对象, 源对象1, 源对象2...);

解释:将obj1 拷贝给 obj2。执行完毕后,obj2 的值会被更新。
作用:将 obj1 的值追加到 obj2 中。如果对象里的属性名相同,会被覆盖。
从语法2中可以看出,Object.assign() 可以将多个“源对象”拷贝到“目标对象”中。
Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
但是 Object.assign()进行的是浅拷贝,拷贝的是对象的属性的引用,而不是对象本身

1
2
3
4
5
let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };
let obj2 = Object.assign({}, obj1);
obj2.person.name = "wade";
obj2.sports = 'football'
console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }

注意:当object只有一层的时候,是深拷贝

展开运算符...

展开运算符是一个 es6 / es2015特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign ()的功能相同。

1
2
3
4
5
6
let obj1 = { name: 'xiaoming', address: { x: 100, y: 100 } }
let obj2 = { ...obj1 }
obj1.address.x = 200
obj1.name = '小明'
console.log(obj1)//{name: "小明", address: {x: 200, y: 100}}
console.log(obj2)//{name: "xiaoming", address: {x: 200, y: 100}}

深拷贝

深拷贝:从堆内存中开辟一个新的区域存放新对象,对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 深拷贝
let obj1 = {
name : '浪里行舟',
arr : [1,[2,3],4],
};
let obj4=deepClone(obj1)
obj4.name = "阿浪";
obj4.arr[1] = [5,6,7] ; // 新对象跟原对象不共享内存
// 这是个深拷贝的方法
function deepClone(obj) {
if (obj === null) return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (typeof obj !== "object") return obj;
let cloneObj = new obj.constructor();
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key]);
}
}
return cloneObj;
}
console.log('obj1',obj1) // obj1 { name: '浪里行舟', arr: [ 1, [ 2, 3 ], 4 ] }
console.log('obj4',obj4) // obj4 { name: '阿浪', arr: [ 1, [ 5, 6, 7 ], 4 ] }

实现深拷贝

递归

递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function deepClone(origin, target) {
let target = target || {},
toStr = Object.prototype.toString

for (let key in origin) {
if (origin.hasOwnProperty(key)) { //排除原型上的对象
if (typeof origin[key] === 'object' && origin[key] !== null) {//对象或数组且不为空
toStr.call(origin[key]) === '[object Array]' //判断object是否为数组
? (target[key] = []) //生成空数组,为了下边的递归
: (target[key] = {})
deepClone(origin[key], target[key])//递归,进行深度拷贝,直到全是原始值
} else target[key] = origin[key] //原始值之间复制不深拷贝
}
}
return target
}

有种特殊情况需注意就是对象存在循环引用的情况,即对象的属性直接的引用了自身的情况,解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。使用WeakMap代替Map,如果是WeakMap的话,就是弱引用关系,当下一次垃圾回收机制执行时,这块内存就会被释放掉。

JSON.parse(JSON.stringify())(不推荐)

用JSON.stringify将对象转成JSON字符串,再用JSON.parse()把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。但是无法拷贝其他引用类型、拷贝函数、循环引用等情况
这种方法虽然可以实现数组或对象深拷贝,但不能处理函数和正则,因为这两者基于JSON.stringify和JSON.parse处理后,得到的正则就不再是正则(变为空对象),得到的函数就不再是函数(变为null)了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Object.prototype.num = 1
let person1 = {
name: '张三',
age: 18,
sex: 'male',
height: 180,
weight: 140,
children: {
first: {
name: '张一',
age: 12,
},
second: {
name: '张二',
age: 10,
},
},
car: ['Benz', 'Mazda'],
}

let str = JSON.stringify(person1)
let person2 = JSON.parse(str)

person2.name = '李四'
person2.children.third = {
name: '张三',
age: 9,
}
person2.car.push('BYD')
console.log(person1, person2)

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!