JS深拷贝与浅拷贝的理解

96 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第13天,点击查看活动详情

前言

首先深拷贝与浅拷贝只是针对引用数据类型而言,如果是基础数据类型则没有这个说法。因为基础数据类型就是简单的赋值。

深拷贝与浅拷贝的区别

对于js里创建的引用类型对象,都会在栈中存放一个该对象的访问地址,然后在堆内存开辟一块空间存放具体的值,这个地址就指向这个值。

浅拷贝

对于引用类型数据而言,浅拷贝拷贝的就是内存地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。因为新旧对象这时候都用的同一个内存地址,如果改变了其中一个对象的值,即改变了堆内存里的值,那么另一个也会改变。

深拷贝

深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象。这时候2个对象的内存地址是不一样的,即堆内存开辟的空间也不一样。所以改变其中一个对象的值,跟另一个对象都没有关系。

实现浅拷贝的方式

直接赋值

let obj1 = {
    name:'jy',
    age:22,
    action:{
        name:'swim'
    }
}
let obj2=obj1
obj2.name='gogo'
obj2.action.name='haha'
console.log('obj1: ', obj1);
console.log('obj2: ', obj2);

console打印结果如下
obj1:  { name: 'gogo', age: 22, action: { name: 'haha' } }
obj2:  { name: 'gogo', age: 22, action: { name: 'haha' } }

...展开运算符

这里需要注意的是,如果这个对象只有一层,那么是深拷贝,如果有多层,那么就是浅拷贝

let obj1 = {
    name:'jy',
    age:22,
    action:{
        name:'swim'
    }
}
let obj2 = {...obj1}
obj1.name='gogo'
obj1.action.name='haha'
console.log('obj1: ', obj1);
console.log('obj2: ', obj2);

image.png

Object.assign()

这个方法同展开运算符一样

let obj1 = {
    name:'jy',
    age:22,
    action:{
        name:'swim'
    }
}
let obj2=Object.assign({},obj1)
obj2.name='gogo'
obj2.action.name='haha'
console.log('obj1: ', obj1);
console.log('obj2: ', obj2);

image.png

Array.prototype.concat()与 Array.prototype.slice()

这2个方法都是如果数组只有一层,那么是深拷贝,如果多层,那么就是浅拷贝

let arr1:any[] =  [
    {
        name: 'gogo'
    },
    'aaa', 
    'bbb', 
];
let arr2 = arr1.concat([]);
// let arr2 = arr1.slice()([]);
arr2[0].name = 'concat';
arr2[1] = 'ccc';
console.log('arr1===>', arr1); 
console.log('arr2===>', arr2); 

image.png

实现深拷贝的方式

JSON.parse(JSON.stringify())

let obj1 = {
    name:'jy',
    age:22,
    action:{
        name:'swim'
    }
}
let obj2=JSON.parse(JSON.stringify(obj1))
obj2.name='JSON.parse'
obj2.action.name='JSON.stringify'
console.log('obj1: ', obj1);
console.log('obj2: ', obj2);

image.png

自己实现一个深拷贝


//用于添加字段接口
 interface LooseObject {
    [key: string]: any
}

/**
 * 深拷贝
 * @param {Object} obj 要拷贝的对象
 * @param {Map} map 用于存储循环引用对象的地址
 */
 function deepClone(obj = {}, map = new Map()) {
    if (typeof obj !== "object") {
      return obj;
    }
    if (map.get(obj)) {
      return map.get(obj);
    }
  
    let result:LooseObject = {};
    // 初始化返回结果
    if (
      obj instanceof Array ||
      // 加 || 的原因是为了防止 Array 的 prototype 被重写,Array.isArray 也是如此
      Object.prototype.toString.call(obj) === "[object Array]"
    ) {
      result = [];
    }
    // 防止循环引用
    map.set(obj, result);
    Object.entries(obj)
     .forEach(key => {
      if (typeof key[1] == 'object') {
        // 递归调用
        result[key[0]] = deepClone(key[1] as object, map);
      }else{
        result[key[0]]=key[1]
      }
     })
    // 返回结果
    return result;
  }

 export default {deepClone}

然后在另一个文件里使用这个方法

import deep from './cloneDeep'
let map=new Map()
let result = deep.deepClone(people,map)
result.name = 'clone'
result.food.weight = 600
console.log('people: ', people);
console.log('result: ', result);

image.png 可以看到2个对象完全不影响,因此实现了深拷贝。

总结

这是对深拷贝与浅拷贝的一点总结,在开发中要注意数据的改变应该用深拷贝还是浅拷贝,多多练习才会运用自如。