浅拷贝和深拷贝的理解以及实现方法

75 阅读5分钟

一、什么是浅拷贝和深拷贝

浅拷贝

  • 对于基本数据类型的成员变量,浅拷贝直接进行值传递,也就是将属性值复制了一份给新的成员变量
  • 对于引用数据类型的成员变量,比如成员变量是数组、某个类的对象等,浅拷贝就是引用的传递,也就是将成员变量的引用(内存地址)复制了一份给新的成员变量,他们指向的是同一个事例。在一个对象修改成员变量的值,会影响到另一个对象中成员变量的值。
  • 如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象

深拷贝

  • 对于基本数据类型,深拷贝复制所有基本数据类型的成员变量的值
  • 对于引用数据类型的成员变量,深拷贝申请新的存储空间,并复制该引用对象所引用的对象,也就是将整个对象复制下来。所以在一个对象修改成员变量的值,不会影响到另一个对象成员变量的值。
  • 在改变新的数组(对象)的时候,不改变原数组(对象)

二、浅拷贝的实现方法

1、Object.assign

Object.assign 是 Object 的一个方法,该方法可以用于 JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝。该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象(也可以是多个来源)。

const obj = {};
const source = {
  name: 'nordon',
  info: {
    age: 18
  }
};

Object.assign(obj, source);
console.log(obj); // {name: 'nordon', info: {…}}

如此很便捷的完成了浅拷贝,接下来可以修改一下数据

source.info.age = 20;
console.log(obj);
console.log(source);

修改之后,obj和source的age都变成了20,满足浅拷贝定义

注意点:

  • 它不会拷贝对象的继承属性;
  • 它不会拷贝对象的不可枚举的属性;
  • 可以拷贝 Symbol 类型的属性。
const obj = {};

const source = {
  name: 'nordon',
  symbol: Symbol()
};

Object.defineProperty(source, 'innumerable' ,{
    value:'innumerable',
    enumerable:false
});

Object.assign(obj, source);
console.log(obj); // {name: 'nordon', symbol: Symbol()}

常见的继承属性toString等也并没有被Object.assign浅拷贝过来

2、扩展运算符

使用扩展运算符也可以完成浅拷贝

const source = {
  name: 'nordon',
  info: {
    age: 18
  }
};
const obj = {...source};

Object.assign(obj, source);
source.info.age = 20;
console.log(obj);
console.log(source);

上述代码和使用Object.assign功能相同,其注意事项也相同,两者在使用上基本是可以相互转换

3、Array.prototype.concat

数组的 concat 方法其实也是浅拷贝,使用场景比较少,使用concat连接一个含有引用类型的数组时,需要注意修改原数组中的元素的属性,因为它会影响拷贝之后连接的数组

const arr = [1, 2, {name: 'nordon'}];
const newArr = arr.concat();
newArr[2].name = 'wy';
console.log(arr); 
console.log(newArr);

会发现arr和newArr索引2的数据都由**{name: 'nordon'}变成了{name: 'wy'}**

4、Array.prototype.slice

数组的 slice 方法其实也是浅拷贝,使用场景比较少,同cancat

const arr = [1, 2, {name: 'nordon'}];
const newArr = arr.slice();
newArr[2].name = 'wy';

三、深拷贝的实现方法

1、使用JSON转换

function deepClone(target) {
	    //通过数据创建JSON格式的字符串
	    let str = JSON.stringify(target);
	    //将JSON字符串转化为JS数据
	    let data = JSON.parse(str);
	    return data;
	}

缺点:

  • JSON转换不能克隆方法;因为JSON格式字符串不支持Function,在序列化的时候会自动删除;
  • JSON转换不能克隆循环引用,如: obj.b.push(obj.c); obj.c.j = obj.b;
  • 会忽略 undefinedsymbol

2、Object.assign()拷贝

es6新增的方法,可用于对象合并,将源对象的所有可枚举属性,复制到目标对象上。

复制代码var data = {
              a: "123",
              b: 123,
              c: true,
              d: [43, 2],
              e: undefined,
              f: null,
              g: function() {    console.log("g");  },
              h: new Set([3, 2, null]),
              i: Symbol("fsd"),
              k: new Map([    ["name", "张三"],    ["title", "Author"]  ])
        };

var newData = Object.assign({},data)
console.log(newData)  

执行结果 1700035714960.jpg 可以看到这个API可以将源对象上的全部数据类型属性值完全复制到一个新的对象上,这难道就是我们所寻找的最完美的深拷贝方式了吗?答案是否,只能说是部分深拷贝,或者说就是浅拷贝,为什么这么说呢,接着往下看。

复制代码var test = {  name: '张三' }
var data = { 
              a: 123,
              b: test
            }
var newData = Object.assign({},data)
console.log(newData) 
// {  a: 123,  b: {    name: '张三'  }}
test.age = 18
console.log(newData)
// {  a: 123,  b: {    name: '张三',   age: 18  }}

结果很明显,这种方式的拷贝,如果源目标对象中某个属性值是对另一个对象的引用,那么这个属性的拷贝仍然是对引用的拷贝,也就是说当对象中只有一级属性,没有二级属性的时候,此方法为深拷贝,但是对象中有对象的时候,此方法,在二级属性以后就是浅拷贝。

3、递归实现

function DeepCopy(obj) {
    // Hash表 记录所有的对象引用关系
    let map = new WeakMap();
    function dp(obj) {
        let result = null;
        let keys = null,
            key = null,
            temp = null,
            existObj = null;
        
        existObj = map.get(obj);
        // 如果这个对象已被记录则直接返回
        if (existObj) {
            return existObj;
        }
        keys = Object.keys(obj);
        result = {};
        // 记录当前对象
        map.set(obj,result);
        for (let i = 0; i < keys.length; i++) {
            key = keys[i];
            temp = obj[key];
            // 如果字段的值也是一个对象则递归复制
            if (temp && typeof temp === 'object') {
                result[key] = dp(temp);
            } else {
                // 否则直接赋值给新对象
                result[key] = temp;
            }
        }
        return result;
    }
    return dp(obj);
}

4、lodash函数库实现深拷贝

lodash很热门的函数库,提供了 lodash.cloneDeep()实现深拷贝

var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false

三、总结

​ 其实了解了以上的方式就已经非常够用了;重点记住,在日常生产环境当中,使用完美方案—lodash.cloneDeep,掌握常用方法,能够解决问题就好。