快速掌握JavaScript深拷贝与浅拷贝

180 阅读10分钟

1. 赋值 & 浅拷贝 & 深拷贝

一句话总结:

  • 浅拷贝:拷贝的是引用的内存地址,所以把A赋值给B,改B,也会把A改了
  • 深拷贝:完全复制出一个新的,完全一样但毫不相关。

(1)赋值

  • 基本类型赋值:为新的变量在栈内存中分配一个新值。
  • 引用类型赋值:新的变量在栈内存中分配一个值,赋的其实是该对象的在栈中的地址,而不是堆中的数据,这个值仅仅是指向同一个对象的引用,和原对象指向的都是堆内存中的同一个对象。
const obj1 = {     name:'estellanini',     age:18,     tel:'15000000000',     arraylist:[1,[3,4],[5,6]],     objectlist:{         id:20,         address:'beijing'     } };

//直接赋值,obj2与obj1完全相同
let obj2 = obj1;
console.log('obj1',obj1);
console.log('obj2',obj2);
 // 此时直接更改obj2中的属性值,观察obj1中的值是否变化
// 结果:更改obj2中的属性值,obj1中的值也跟随发生变化
// 原理:当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。
//      两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容
let obj2 = obj1;
obj2.name = "jenny";
obj2.arraylist[2]=[8,9,10];
obj2.objectlist.address="tianjin";
console.log('obj1',obj1); //更改后的
console.log('obj2',obj2); //更改后的

(2)浅拷贝

  • 一句话概括:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
  • 理解:如果是对象类型,则只拷贝一层,如果对象的属性又是一个对象,那么此时拷贝的就是此属性的引用
  • 为什么要存在浅拷贝,不直接全部都是深拷贝呢?
    • 性能与内存开销
  • 什么情况下会使用浅拷贝呢?

(3)深拷贝

  • 一句话概括:深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。拷贝前后两个对象互不影响。
  • 理解:浅拷贝是只拷贝一层,深拷贝会拷贝所有的属性。深拷贝前后两个对象互不影响
  • 常见的深拷贝库方法:lodash-cloneDeep

2. 手写浅拷贝

(1)手写循环

  • 在该对象中循环判断hasOwnProperty是否有某属性,若有,则赋值给新对象。
  • 如果 object 具有带指定名称的属性,则 hasOwnProperty 方法返回 true,否则返回 false。此方法不会检查对象原型链中的属性;该属性必须是对象本身的一个成员。
  • 效果:更改基本类型,新对象与原对象无关,互不影响。更改引用类型,更改的是引用,所以会互相影响,更改一个另一个也会变。
  • 注意:这里仅复制第一层属性即可。
const obj1 = {
    name:'estellanini',
    age:18,
    tel:'15000000000',
    arraylist:[1,[3,4],[5,6]],
    objectlist:{
        id:20,
        address:'beijing'
    }
};

const shallowCopy = (obj) => {
    const newObj={};
    for(let prop in obj){
        //console.log(prop); // 此处输出的是name age tel arraylist objectlist
        if(obj.hasOwnProperty(prop)){
            newObj[prop]=obj[prop];
        }
    }
    return newObj;
}
const obj3 = shallowCopy(obj1);
obj3.name = "jenny"; // 修改后,obj1的此属性值不变
obj3.arraylist[2]=[8,9,10]; // 修改后,obj1的此属性值改变
obj3.objectlist.address="shanghai"; // 修改后,obj1的此属性值改变
console.log(obj1)
console.log(obj3)

(2)使用Object.assign进行浅拷贝

  • Object.assign(target,sources):target代表目标对象,sources代表原对象,返回值是目标对象。
// 使用Object.assign进行浅拷贝,直接更改obj3中的属性值,观察obj1中的变化
// 结果:更改obj3中的属性值,在obj1中,基本类型值没有变化,但引用类型值发生了变化
// 原理:如果是对象类型,则只拷贝一层,如果对象的属性又是一个对象,那么此时拷贝的就是此属性的引用
let obj3 = Object.assign({}, obj1);
obj3.name = "jenny"; //修改后,obj1的此属性值不变
obj3.arraylist[2]=[8,9,10]; //修改后,obj1的此属性值改变
obj3.objectlist.address="tianjin"; //修改后,obj1的此属性值改变
console.log('obj1',obj1);
console.log('obj3',obj3);

(3)数组浅拷贝Array.prototype.concat()

  • concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
const arr1 = [1,2,{
    'name':'estellanini',
    'age':18,
}];

//数组浅拷贝 Array.prototype.concat(),直接更改arr2数组中的某个值,观察arr1中的该值是否变化
//结果:更改arr2中的属性值,在arr1中,基本类型值没有变化,但引用类型值发生了变化
let arr2 = arr1.concat();
arr2[0]=99; //修改后,arr1的此属性值不变
arr2[2].name='lala'; //修改后,arr1的此属性值改变
console.log(arr1);
console.log(arr2);

(4)数组浅拷贝 Array.prototype.slice()

  • slice() 方法返回一个新的数组对象,这一对象是一个由 begin 和 end 决定的原数组的浅拷贝(包括 begin,不包括end)。原始数组不会被改变。
// 数组浅拷贝 Array.prototype.slice(),直接更改arr2数组中的某个值,观察arr1中的该值是否变化
// 结果:更改arr3中的属性值,在arr1中,基本类型值没有变化,但引用类型值发生了变化
let arr3 = arr1.slice();
arr3[0]=99; //修改后,arr1的此属性值不变
arr3[2].name='lala'; //修改后,arr1的此属性值改变
console.log(arr1);
console.log(arr3);

(5)使用扩展运算符进行浅拷贝

// 结果:更改arr4中的属性值,在arr1中,基本类型值没有变化,但引用类型值发生了变化
let arr4=[...arr1];
arr4[0]=99; //修改后,arr1的此属性值不变
arr4[2].name='lala'; //修改后,arr1的此属性值改变
console.log(arr1);
console.log(arr4);

(6)函数库lodash

_.clone(value)

3. 手写深拷贝

  • 简单版深拷贝实现:
// obj为要拷贝的对象
const deepClone = (obj={}) => {
    if(typeof obj !== 'object' || obj == null){
    // 排除obj是null、以及非对象数组
        return obj;
    }
    // 初始化返回结果
    let result;
    if(obj instanceof Array){
        result = [];
    } else {
        result = {};
    
    }
    for (let key in obj) {
        // 保证key不是原型链的属性
        if(obj.hasOwnProperty(key)){
            result[key] = deepClone(obj[key]); 
        }
    }
    return result;
}
  • 下面来具体分析深拷贝有哪些实现方式:这里给一个较为全面的测试数据
// 测试数据:obj1对象包括基本类型以及引用类型
const obj1 = {
    'name':'estellanini',
    'age':18,
    'tel':'15000000000',
    'arraylist':[1,[3,4],[5,6]],
    'objectlist':{
        'id':20,
        'address':'beijing'
    },
    'date': new Date(),
    'reg': new RegExp('\w+'),
    'err': new Error('error message'),
    'map': new Map([
        ['name', '张三'],
        ['title', 'Author']
    ]),
};

(1)JSON.parse(JSON.stringify()) 序列化反序列化

  • 利用 JSON.stringify 将js对象序列化(JSON字符串),再使用JSON.parse来反序列化(还原)js对象;序列化的作用是存储和传输。
  • 只适用于一般数据的拷贝(对象、数组)。
  • 存在的问题:
    • 如果JSON里面有时间对象,则序列化结果:时间对象=>字符串的形式,而不是时间对象

    • 如果JSON里有RegExp、Error、Set、Map对象,则序列化的结果将只得到空对象

    • 如果JSON里有 function、undefined,则序列化的结果会把 function、undefined 丢失

    • 如果JSON里有NaN、Infinity和-Infinity,则序列化的结果会变成 null

    • 如果JSON里有对象是由构造函数生成的,则序列化的结果会丢弃对象的 constructor

    • 如果对象中存在循环引用的情况也无法实现深拷贝,直接报错 TypeError: Converting circular structure to JSON

      // 验证循环检测
      var a={};
      a.a=a;
      console.log(DeepCopy(a)) // Uncaught TypeError: Converting circular structure to JSON
      
    • 递归爆栈问题

    • 引用丢失问题:假如一个对象a,a下面的两个键值都引用同一个对象b,经过深拷贝后,a的两个键值会丢失引用关系,从而变成两个不同的对象

      // 深拷贝方法之JSON.parse(JSON.stringify(obj))
      // 结果:RegExp、Error序列化的结果将只得到空对象
      // 时间对象,则序列化结果:时间对象=>字符串的形式,而不是时间对象
      // JSON.parse(JSON.stringify(obj))会存在多种问题
      // 只适用于一般数据的拷贝(对象、数组)
         function DeepCopyJSON(obj) {
           return JSON.parse(JSON.stringify(obj))
         }       
      

(2)递归实现

  • 【初版】

    • 存在的问题:

      • 判断是否对象的逻辑不够严谨
      • 没有对参数做检验
      • 递归爆栈问题
      • 引用丢失问题
      • 循环引用问题
      • 没有考虑数组的兼容
       function DeepCopy1(obj){
           const newObj={};
           for(let prop in obj){
               if(obj.hasOwnProperty(prop)){
                   if(typeof obj[prop]==='object'){
                       newObj[prop]=DeepCopy1(obj[prop]);
                   }else{
                       newObj[prop]=obj[prop];
                   }
               }
           }
           return newObj;
       }
      
    • 初版存在问题1“判断是否对象的逻辑不够严谨”的解决方法:Object.prototype.toString.call(x)

      • 在JavaScript里使用typeof判断数据类型,只能区分基本类型,即:number、string、undefined、boolean。
      • 对于null、array、function、object来说,使用typeof都会统一返回object字符串。
      • 要想区分对象、数组、函数、单纯使用typeof是不行的。在JS中,可以通过Object.prototype.toString方法,判断某个对象之属于哪种内置类型。
      • 分为null、string、boolean、number、undefined、array、function、object、date、math。
       function isObject(x) {
           //强烈注意,后面的Object是大写
           return Object.prototype.toString.call(x)==='[object,Object]';
       }
      
    • 初版存在问题2“没有对参数做检验”的解决方法:

       if (!isObject(obj)) {
           return obj;
       }
      
    • 初版存在问题3“递归爆栈问题”的解决方法:

       // 递归方法最大的问题在于爆栈,当数据的层次很深时就会栈溢出
       // 下面代码可以生成指定深度和每层广度的代码
       function createData(deep, breadth) {
           var data = {};
           var temp = data;
      
           for (var i = 0; i < deep; i++) {
               temp = temp['data'] = {};
               for (var j = 0; j < breadth; j++) {
                   temp[j] = j;
               }
           }
           return data;
       }
      
       const test=createData(1, 3); // 1层深度,每层有3个数据 {data: {0: 0, 1: 1, 2: 2}}
       createData(3, 0); // 3层深度,每层有0个数据 {data: {data: {data: {}}}}
       console.log(test)
      
  • 【改版1】

    • 解决:“判断是否对象的逻辑不够严谨”这个问题
    • 但仍存在以上其他所有问题
function isObject(x){
    return Object.prototype.toString.call(x)==='[object,Object]'; //强烈注意,后面的Object是大写
}

function DeepCopy2(obj){
    const newObj={};
    for(let prop in obj){
        if(obj.hasOwnProperty(prop)){
            if(isObject(obj[prop])){
                newObj[prop]=DeepCopy2(obj[prop]);
            }else{
                newObj[prop]=obj[prop];
            }
        }
    }
    return newObj;
}

(3)循环实现

  • 【改版2】

    • 存在问题:RegExp、Error序列化的结果将只得到空对象

    • 时间对象,序列化结果:时间对象=>字符串的形式,而不是时间对象

    • 同时,也没有解决循环引用的问题

       function DeepCopyLoop(x) {
           const root = {};
           // 栈
           const loopList = [{
                   parent: root,
                   key: undefined,
                   data: x,
           }];
      
           while(loopList.length) {
               // 深度优先
               const node = loopList.pop();
               const parent = node.parent;
               const key = node.key;
               const data = node.data;
      
               // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
               let res = parent;
               if (typeof key !== 'undefined') {
                   res = parent[key] = {};
               }
      
               for(let k in data) {
                   if (data.hasOwnProperty(k)) {
                       if (typeof data[k] === 'object') {
                           // 下一次循环
                           loopList.push({
                               parent: res,
                               key: k,
                               data: data[k],
                           });
                       } else {
                           res[k] = data[k];
                       }
                   }
               }
           }
      
           return root;
       }
      
      • 没有解决循环引用的问题:
        //验证循环检测
        var   a={};
        a.a=a;
        console.log(DeepCopy(a))  //Uncaught TypeError: Converting circular structure to JSON
      
  • 【改版3】

    • 解决以下问题:属性是基本类型、属性是对象、属性是数组、循环引用的情况,比如 obj.prop1 = obj
    • 仍存在的问题:一些特殊类型的对象,比如 Date, 正则,Set,Map等没有处理、使用typeof 来判断是否是对象是有问题的,typeof null 的结果也是 'object'
       function DeepCopy3(originObj, map = new WeakMap()) {
           // 判断是否为基本数据类型
           if(typeof originObj === 'object') {
               // 判断是都否为数组
               const cloneObj = Array.isArray(originObj) ? [] : {};
               // 判断是否为循环引用
               if(map.get(originObj)) {
                   return map.get(originObj);
               }
               map.set(originObj, cloneObj);
               for(const prop in originObj) {
                   cloneObj[prop] = DeepCopy3(originObj[prop], map);
               }
               return cloneObj;
           } else {
               return originObj;
           }
       }
      
       obj1.obj2 = obj1;
       const aa = DeepCopy3(obj1);
       console.log(aa);
      
  • 【改版4】

    • 解决版本3的问题:
       function DeepCopy4(originObj, map = new WeakMap()) {
           // 判断是否为基本数据类型
           if(isObject_v2(originObj)) {
               // 判断是否为循环引用
               if(map.get(originObj)) {
                   return map.get(originObj);
               }
      
               // 判断是否为几种特殊需要处理的类型
               let type = [Date, RegExp, Set, Map, WeakMap, WeakSet];
               if(type.includes(originObj.constructor)) {
                   return new originObj.constructor(originObj);
               }
               // 其他类型
               let allDesc = Object.getOwnPropertyDescriptors(originObj);
               let cloneObj = Object.create(Object.getPrototypeOf(originObj), allDesc);
      
               // Reflect.ownKeys 可以获取到
               for(const prop of Reflect.ownKeys(originObj)) {
                   cloneObj[prop] = isObject_v2(originObj[prop]) && typeof originObj[prop] !== 'function' ? DeepCopy4(originObj[prop], map) : originObj[prop];
               }
               return cloneObj;
           } else {
               return originObj;
           }
       }
      
       // 是否为引用类型
       function isObject_v2(obj) {
           return typeof obj === 'object' || typeof obj === 'function' && obj !== null;
       }