如何实现一个对象深拷贝
2020-9-31
456

js里面有两种数据类型,基本数据类型和引用数据类型;而这两大数据类型的存储方式又不一致。基本数据类型是存储在栈内存;引用数据类型的值是存储在堆内存中,关于堆栈内不熟悉的自行查阅。

基本数据类型

String、Number、Boolean、undefined、null、symbol

引用数据类型

Array、Date、RegExp、Function、Objcet、Error、Set、Map

因为基本数据类型是按值访问,可以直接获取到值,如果需要拷贝数据,直接获取一份数据保存在另外一个变量即可。而对于引用数据类型,是无法直接获取到值的,我们获取到的往往是存储在堆内存的地址。关于概念的问题这里不细说,主要还是为了整理如何实现对象拷贝。

浅拷贝

object浅拷贝

方法一、Object.assign(target,...sources):

这个方法主要是用于两个对象合并,举个栗子:

let obj1 = { a: 1, b: 2 };
let obj2 = { b:22, c: 3 };
Object.assign(obj1,obj2);
console.log(obj1);  // { a: 1, b: 22, c: 3 }
console.log(obj2);  // { b:22, c: 3 }

创建2个对象obj1和obj2,从输出可以看出,obj2的内容合并到了obj1上,而且覆盖了b的值,也就是说该方法会改变第一个参数的值,而后续传递的参数并无影响。利用这个原理我们可以实现一个浅拷贝

let obj1 = { a:1, b: 2 };
let cloneObj = {};
Object.assign(cloneObj,obj1);
obj1.a = 11;
console.log(cloneObj) // { a: 1, b: 2 }
console.log(obj1) // { a: 11, b: 2 }

方法二、ES6的结构运行符 ...

let obj1 = { a: 1, b: 2 };
let cloneObj = {...obj1};
obj1.a = 11;
console.log(cloneObj);  //{ a: 1, b: 2 }
console.log(obj1) //{ a: 11, b: 2 }
Array浅拷贝

方法一、Array.prototype.slice(startIndex,endIndex);

**slice()** 方法返回一个新的数组,是一个由 beginend 决定的原数组的浅拷贝(包括 begin,不包括end)。原始数组不会被改变。

let arr = [1,2,3];
let cloneArr = arr.slice(0,3);
arr[0] = 2;
console.log(arr);  // [2, 2, 3]
console.log(cloneArr); // [1, 2, 3]

方法二、Array.prototype.concat(...source);

**concat()** 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。

let arr = [1,2,3];
let cloneArr = Array.concat(arr);
arr[1] = 22;
console.log(arr);  // [1, 22, 3]
console.log(cloneArr); // [1, 2, 3]

方法三、扩展运算符...

let arr = [1,2,3];
let [...cloneArr] = arr;
let cloneArr1 = [...arr];
arr[1] = 22;
console.log(arr); // [1, 22, 3]
console.log(cloneArr); // [1, 2, 3]
console.log(cloneArr1); // [1, 2, 3]

深拷贝

方法一、JSON.stringify/JSON.parse

因为字符串是存储在栈内存中,所以可以把一个引用数据类型的值转换为字符串存储起来,然后在转换为引用类型用一个变量来存储新的对象。

定义一个对象

function Obj() {
    this.func = function () {
        alert(1) 
    };
    this.obj = {a:1};
    this.arr = [1,2,3];
    this.und = undefined;
    this.reg = /123/;
    this.date = new Date(0);
    this.NaN = NaN
    this.infinity = Infinity
    this.sym = Symbol(1)
}
let obj1 = new Obj();
/* json序列化实现深拷贝 */
let str = JSON.stringify(obj1);
let cloneObj = JSON.parse(str);
obj1.obj.a = 2;
console.log(obj1);
console.log(cloneObj);

企业微信截图_cb0be0dc-2358-4d0b-b32a-93b9eefb5f79

从打印结果可以看出obj1修改了obj里的属性,但并未影响到拷贝后的对象;但也暴露了一些问题,少了一些内容;这也是使用JSON.stringify()方法深拷贝对象的缺陷,但是对于日常的开发需求(对象和数组),使用这种方法是最简单和快捷的。

使用JSON.stringifiy需要注意一下几点:

  1. 拷贝的对象的值中如果有函数,undefined,symbol则经过JSON.stringify()序列化后的JSON字符串中这个键值对会消失
  2. 无法拷贝不可枚举的属性,无法拷贝对象的原型链
  3. 拷贝Date引用类型会变成字符串
  4. 拷贝RegExp引用类型会变成空对象
  5. 对象中含有NaN、Infinity和-Infinity,则序列化的结果会变成null
  6. 无法拷贝对象的循环应用(即obj[key] = obj)

但在真实的开发过程中,接口数据结构往往都是很深层的,而且还会包含Date引用类型,undefined、function的,这时就需要我们自己手动实现一个深拷贝了。当然对于深拷贝有很多优秀的库,兼容做的也很好,如果不想造轮子可以直接使用工具库,lodash或者是Jquer;

手动实现简易版本深拷贝

首先肯定是要遍历对象里面的每一项,如果value是引用数据类型,那么需要继续做处理,如果value是基本数据类型,则直接赋值;实现如下:

function deepClone(obj) {
		let newObj = {};
		for(let key in obj) {
				if(typeof obj[key] !== "object") {  // 判断value的数据类型
						newObj[key] = obj[key];   //非object类型直接赋值
				} else {
						newObj[key] = deepClone(obj[key]);    //object类型继续递归
				}
		}
		return newObj;
}

let cloneObj = deepClone(obj1);
obj1.obj.a = 2;
obj1.arr[0] = 11;
console.log(obj1);
console.log(cloneObj);

企业微信截图_597ffd11-69f5-4916-9b5a-f0d372ffad28

从输出结果分析,首先obj实现了拷贝,但是arr却变成了对象,date也变为了空对象,reg也是空对象,对于非正常键值对的对象都被转译为了空对象,对于这种情况只能用判断来做兼容。

deepClone(obj) {
      let isBase = (obj) => typeof obj !== "object";
      if (obj === "null") {
        return obj;
      }
      if (obj instanceof RegExp) {
        return new RegExp(obj);
      }
      if (obj instanceof Array) {
        return obj.map((item) => {
          return deepClone(item);
        });
      }
      if (obj instanceof Date) {
        return new Date(obj);
      }
      if (!isBase(obj)) {
        let newObj = {};
        for (let key in obj) {
          if (typeof obj[key] === "object") {
            newObj[key] = deepClone(obj[key]);
          } else {
            newObj[key] = obj[key];
          }
        }
        return newObj;
      } else {
        return obj;
      }
    },

这个是自己实现的一个兼容版本的深拷贝,目前对于Data对象,Array对象和RegExp对象都做了兼容,目前这个方法写的也很简单,对象对象原型链的继承还是存在问题的。如果有这个需求,也是可以继续做兼容的;目前这个方法还可以优化,但能力有限,在写这篇文章的时候也看了很多实现深拷贝的问题,遇到了很多知识点都不太理解,导致写着写着就去深追知识点了。所以索性先写出一个初版,后续优化会及时更新。