十分钟带你吃透js中的“深浅拷贝”

我心飞翔 分类:javascript

说在前面的话

在日常开发项目中,我们会经常遇到这样的问题,例如: 需要复制一份对象或数组,但是又不能改变原始对象,咋办呢?这时js的深浅拷贝就派上用场了。在JS中,除了基本数据类型,其他一切皆对象。

想要了解 “深浅拷贝” 我们首先得要了解js的“数据类型” 及 “栈区和堆区”。

数据类型

  • 基本数据类型 (String, Number, Boolean, null, undefined, Symbol, BigInt)
  • 引用数据类型 (Object, Array, Data, Math, RegExp)

内存中的栈区(stack)和堆区(heap)

咱们知道,“基本数据类型”在内存中占有固定大小的空间,通过按值访问。“引用数据类型”在内存中大小不固定,所以其在栈中只存放堆内存地址(内存地址是大小固定的),指向的却是堆中的对象,所以它是按引用访问的。假如需要获取“引用数据类型”中的变量,需要先访问栈内存中的地址,然后根据该地址才能获取到相对应的值。

栈区:会自动分配内存空间,自动释放,占用固定大小空间,存放基本类型。
堆区:会动态分配内存空间,大小不固定且不会自动释放,存放引用类型。

基本数据类型(栈区)

let a = 100;
let b = a;
b = 200;
console.log(a);  // a = 100
console.log(b);  // b = 200
注释:基本数据类型是按值存放的,故可以直接 “按值访问”,上述a赋值给b,对b数据做修改后,是不会影响原始数据的值。
 

引用数据类型(堆区)

let a = {
    name: 'voledy',
    age: 18
};
let b = a;
b.name = '哇哈哈';
console.log(a);  // a = {name: '哇哈哈', age: 18}
console.log(b);  // b = {name: '哇哈哈', age: 18}
注释:因为a是引用类型,a赋值给b,其实就是将a的内存地址指向b,它们指向的是同一内存地址,所以b的属性改了即a的属性也会跟着变。
 

浅拷贝和深拷贝

用最简单的一句话概括:当你在目标对象中修改一个值的时候,看对原对象是否有影响。如果有影响那就是浅拷贝,如果没有那就深拷贝。

注:这里我们所说的都是对于引用类型,对于基本数据类型而言并没有 “深浅拷贝” 的区别

上面说了一大堆,估计你都看的有点烦了,那现在我们就开始进入今天的正题吧!!!

一 浅拷贝

1. Object.assign('目标对象', obj1, obj2...)

//当定义的对象只有基本类型时,该方法就是深拷贝。
let a = {
    name: 'voledy',
    age: 18
};
let b = Object.assign({}, a);
b.name = "哇哈哈";
console.log(a);   // a = {name: 'voledy', age: 18};
console.log(b);   // b = {name: '哇哈哈', age: 18};

//当定义的对象中有引用类型的时,该方法就是浅拷贝。
let a = {
    name: 'voledy',
    age: 18,
    eat:{
        type: '苹果',
        price: 18,
    }
};
let b = Object.assign({}, a);
b.name = "哇哈哈";
b.eat.type = "西瓜";
b.eat.price = 30;
console.log(a);  // a = {name: 'voledy', age: 18, eat:{ type: '西瓜', price: '30'}};
console.log(b);  // b = {name: '哇哈哈', age: 18, eat:{ type: '西瓜', price: '30'}};
 

2. ES6中的扩展运算符

//我们通过使用扩展运算符(...)也能实现深拷贝。该方法和上述方法一样只能用于深拷贝第一层的值,当拷贝第二层的值时
仍是引用同一个内存地址。
let a = {
    name: 'voledy',
    age: 18
};
let b = {...a};
b.name = "哇哈哈";
console.log(a);   // a = {name: 'voledy', age: 18};
console.log(b);   // b = {name: '哇哈哈', age: 18};

-----------------------------  分割线  -----------------------------

let a = {
    name: 'voledy',
    age: 18,
    eat:{
        type: '苹果',
        price: 18,
    }
};
let b = {...a};
b.name = "哇哈哈";
b.eat.type = "西瓜";
b.eat.price = 30;
console.log(a);  // a = {name: 'voledy', age: 18, eat:{ type: '西瓜', price: '30'}};
console.log(b);  // b = {name: '哇哈哈', age: 18, eat:{ type: '西瓜', price: '30'}};
 

3. 数组中的方法:Array.prototype.slice() 和 Array.prototype.concat()

//该方法只能用于深拷贝第一层的值,当拷贝第二层的值时仍是引用同一个内存地址。
------------------------ 代码就不写了,有兴趣的可以自己去试下... ------------------------
 

总结
浅拷贝会在栈中开辟一个新的内存空间,将原对象一级中的“基本数据类型”复制一份到新的内存空间,所以相互不影响。当对象中有“引用类型”时,它只能拷贝“引用类型”在堆内存中的地址,所以赋值后会影响原对象的值。

二 深拷贝

1. JSON.parse(JSON.stringify(obj))

//该方法可以实现深拷贝。 但是需要注意
1:会忽略undefined,Symbol,函数。
2:在处理new Date() 会出错
3:循环引用会出错
4:不能处理正则,拷贝的是一个空对象
5:继承的属性会丢失
一句话概括:可以转成 JSON 格式的对象才能使用这种方法。

let a = {
    name: 'voledy',
    age: 18,
    fn: function(){},
    from: undefined,
    to: Symbol('深圳'),
    nums: /'g'/,
    eat:{
        type: '苹果',
        price: 18,
    }
};
let b = JSON.parse(JSON.stringify(a));
b.name = "哇哈哈";
b.eat.type = "西瓜";
b.eat.price = 30;
console.log(a);  // a = {name: 'voledy', age: 18, nums: /'g'/, fn: function(){}, eat:{ type: '苹果', price: '18'}};
console.log(b);  // b = {name: '哇哈哈', age: 18, nums: {}, eat:{ type: '西瓜', price: '30'}};
 

2. 使用递归函数

//写一个递归函数
deepCopy = (source) =>{
    const targetObj = source.constructor === Array ? [] : {}; // 先判断是数组还是对象
    for(let keys in source){  // 遍历
        if(source.hasOwnProperty(keys)){
            if(source[keys] && typeof source[keys] === 'object'){ // 如果值是对象,直接递归
                targetObj[keys] = source[keys].constructor === Array ? [] : {};
                targetObj[keys] = this.deepCopy(source[keys]);
            }else{ // 如果不是,就直接赋值
                targetObj[keys] = source[keys];
            }
        }
    }
    return targetObj;
};

let a = {
    name: 'voledy',
    age: 18,
    eat:{
        type: '苹果',
        price: 18,
        tt:{
            aaa: 1000
        }
    }
};
let b = this.deepCopy(a);
b.eat.type = '西瓜';
b.eat.tt.aaa = '666666';
console.log(a) // a = {name: 'voledy', age: 18,  eat:{ type: '苹果', price: '18', tt:{aaa: 1000}}};
console.log(b) // b = {name: 'voledy', age: 18,  eat:{ type: '西瓜', price: '18', tt:{aaa: 666}}};
 

3. 使用第三方函数库 lodash

总结
深拷贝会在堆中开辟一个新的内存空间,将原对象或数组完整的拷贝一份在新的空间中,两者互不影响,老死而不相往来。

作者:掘金-十分钟带你吃透js中的“深浅拷贝”

回复

我来回复
  • 暂无回复内容