浅拷贝和深拷贝

一、引言

在js中,数据分为基础类型引用类型两大类:

6种基础类型:

  1. number类型

  2. string类型

  3. boolean类型

  4. symbol类型

  5. null类型

  6. undefined类型

1种引用类型:

  1. object类型

对于基础类型的数据,其大小是固定不变的,所以变量名和值都存储在栈内存中的。对于引用类型的数据,其大小可能会变化,所以变量名和数据在堆内存中的地址存储在栈内存,数据本身存储在堆内存中。

let a = 10
let obj1 = {
    m: 111
}

对于上面两个变量,在内存中是以下面这种方式存放的:
在这里插入图片描述

可以看出变量a的值是基础类型,所以直接存在了栈内存中,而obj1的值是引用类型,它在栈内存中的值是一个地址(地址是编的,不是真实地址值),指向堆内存中存储的对象本身。

如果我们分别把变量a和obj1复制一份

let a = 10
let b = a;

let obj1 = {
    m: 111
}
let obj2 = obj1;

那么原始变量和复制后的变量在内存中的结构就是下面这种:

在这里插入图片描述

出现这种现象的原因是因为,对于基础类型,=号的操作是值传递操作,直接把a的复制一份给b,而对于引用类型,=号的操作是址传递操作,obj1把自己的堆内存地址复制了一份给obj2,所以obj1和obj2指向的是堆内存中的同一个数据。

这个时候我们如果修改原始变量的值,基础类型的不会受影响,而引用类型却会被影响:

let a = 10
let b = a;
a = 20;

console.log(a); // 20
console.log(b); // 10

let obj1 = {
    m: 111
}
let obj2 = obj1;
obj1.m = 222;

console.log(obj1.m); // 222
console.log(obj2.m); // 222

这种互相影响是我们不想要的,我们想要的复制是obj1和obj2在内存中是分开存储的,就像下面这种:

在这里插入图片描述

这种情况下,obj1和obj2是两个独立的数据,不会互相影响。要实现这种复制,就需要浅拷贝和深拷贝了。

对于基础数据来说,=号操作符就是值传递,会在栈内存中分别保存,不存在深浅拷贝的说法,只有引用类型数据才有深浅拷贝的说法。

二、浅拷贝

浅拷贝是只复制了一层的数据,对于更深层次的数据,还是址传递。

let obj1 = {
    a: 10,
    b: {
        m: 20
    }
}
let obj2 = shallowCopy(obj1); // 假设的浅拷贝函数

假设我们对上面的obj1对象,使用了某种浅拷贝方法,得到了obj2,那么它们在内存中的关系就是下图这种:

在这里插入图片描述

1. Object.assign()

Object.assign()会把一个对象的可枚举属性逐一复制一份给另一个对象,但是如果对象的属性值是引用类型的话,那也只是复制引用地址:

let obj1 = {
    a: 10,
    b: {
        m: 20
    }
}
let obj2 = Object.assign({}, obj1);

console.log(obj2.a); // 10
console.log(obj2.b.m); // 20

obj1.a = 100;
obj1.b.m = 200;

console.log(obj2.a); // 10
console.log(obj2.b.m); // 200

可以看出在使用Object.assign()浅拷贝之后,属性a是基础类型,所以修改obj1.a不会影响obj2.a,而b属性是引用类型,修改obj1.b.m的值会影响obj2.b.m。

2. 扩展运算符
let obj1 = {
    a: 10,
    b: {
        m: 20
    }
}
let obj2 = {...obj1}

console.log(obj2.a); // 10
console.log(obj2.b.m); // 20

obj1.a = 100;
obj1.b.m = 200;

console.log(obj2.a); // 10
console.log(obj2.b.m); // 200

es6的…扩展运算符用在对象上也是浅拷贝,扩展运算符也可以用在数组上。

3. for…in

使用for…in实现简单的浅拷贝:

function shallowCopy(obj) {
    let result = Array.isArray(obj) ? [] : {};
    for (let key in obj) {
        result[key] = obj[key];
    }
    return result;
}

let obj1 = {
    a: 10,
    b: {
        m: 20
    }
}

let obj2 = shallowCopy(obj1);

console.log(obj2.a); // 10
console.log(obj2.b.m); // 20

obj1.a = 100;
obj1.b.m = 200;

console.log(obj2.a); // 10
console.log(obj2.b.m); // 200

for…in只遍历了第一层的属性,所以这里也是浅拷贝,这个函数对数组也是浅拷贝,这里就不演示了。

4. slice()和concat()

slice()和concat()属于数组的方法,对数组也可以实现浅拷贝:

let arr1 = [1, 2, [3, 4]]

let arr2 = arr1.slice();
let arr3 = arr1.concat();

console.log(arr2); // [1, 2, [3, 4]]
console.log(arr3); // [1, 2, [3, 4]]

arr1[0] = 10;
arr1[2][0] = 30;

console.log(arr2); // [1, 2, [30, 4]]
console.log(arr3); // [1, 2, [30, 4]]

三、深拷贝

了解了浅拷贝的效果,现在大概也能猜到深拷贝是什么样了,其实就是所有对所有层次的属性都进行复制一份备份数据,互相独立,任何层次属性的修改都不会影响另一个对象。

let obj1 = {
    a: 10,
    b: {
        m: 20
    }
}
let obj2 = "深拷贝"(obj1)

同样假设对上面这个对象,使用某种深拷贝的方法,得到obj2,那么它们在内存中的关系就如图下所示:

在这里插入图片描述

1. JSON字符串

JSON.stringify()是将传入的数据转为JSON字符串的方法,JSON.parse()是将JSON字符串转为js数据的方法,通过这两种方法的组合,就能实现深拷贝的效果了:

let obj1 = {
    a: 10,
    b: {
        m: 20
    }
}
let arr1 = [1, 2, [3, 4]];

let obj2 = JSON.parse(JSON.stringify(obj1));
let arr2 = JSON.parse(JSON.stringify(arr1));

obj1.a = 100;
obj1.b.m = 200;
arr1[0] = 10;
arr1[2][0] = 30;

console.log(obj2.a); // 10
console.log(obj2.b.m); // 20
console.log(arr2); // [1, 2, [3, 4]]

这个方法有一个问题,就是不能复制函数类型的数据。

2. 递归函数

在上面我们看到浅拷贝都是只拷贝第一层属性值或数组值,如果值是引用地址,那也只是复制地址,不会深入内部复制数据。要手动实现深拷贝,一个思路就是探索对象或数组的每一层,对每一层都是用浅拷贝,这样,就可以达到深拷贝的效果。

下面是一种简单的深拷贝实现:

function deepCopy(data) {
    // 判断拷贝的是对象还是数组,初始化一个变量保存结果
    let result = Array.isArray(data) ? [] : {};
    
    // 如果是对象或者数组类型,就遍历厘里面的属性或元素
    if (data !== null && typeof data === 'object') {

        // 判断是数组还是
        if (Array.isArray(data)) {
            data.forEach(index => {
                result[index] = deepCopy(data[index])
            })
        }else {
            Object.keys(data).forEach(key => {
                result[key] = deepCopy(data[key]);
            })
        }
    }else {
        // 如果是基本类型或者函数类型的数据,则直接把data保存在result里
        result = data;
    }
    // 返回结果
    return result;
}

let obj1 = {
    a: 10,
    b: {
        m: {
            n: 20
        }
    },
    c: function () {
        console.log('before');
    }
}


let obj2 = deepCopy(obj1);

obj1.a = 100;
obj1.b.m.n = 200;
obj1.c = function () {
    console.log("after");
}

console.log(obj2.a); // 10
console.log(obj2.b.m.n); // 20
obj2.c(); // before

使用递归函数进行深拷贝之后,不仅属性可以被拷贝,方法也能被拷贝,修改属性和方法都不影响另一个,这个函数对数组也有效,这里就不再演示。

3. $.extend()

这个方法时jQuery的方法,也能实现深拷贝:

let obj1 = {
    a: 10,
    b: {
        m: {
            n: 20
        }
    },
    c: function () {
        console.log('before');
    }
}

let obj2 = $.extend(true, {}, obj1)


obj1.a = 100;
obj1.b.m.n = 200;
obj1.c = function () {
    console.log("after");
}

console.log(obj2.a); // 10
console.log(obj2.b.m.n); // 20
obj2.c(); // before

$.extend()当做深拷贝使用时有三个参数,第一个判断是否要深拷贝,第二个参数是目标对象,第三个是源对象。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值