刚接触前端 不了解的深拷贝

158 阅读6分钟

作为一个刚学前端不久的小菜菜,最近用了很多次loadsh的深拷贝,就想着自己实现一下深拷贝,了解一下基础的实现。

5f35c47badf75efd.jpg

就先从值类型和引用类型开始

1. 数据类型

1.1 值数据类型

保存在栈内存中的简单的数据类型,包含NumberStringBooleanNullUndefined

1.2 引用数据类型

保存在堆内存中的对象,所以引用类型值保存的是一个指针,这个指针指向存在堆中的一个对象。

2. 浅拷贝

被拷贝引用类型,只拷贝了一层,开辟了新的内存空间把相应的值保存复制到了新的内存空间里,对于第一层中的引用类型,复制了他们的内存地址,而不是值。

2.1 对于数组

2.1.1 扩展运算符

const arr = [1,2,3,{a: 1}];
​
const arr1 = [...arr];
​
arr1[3].a = 2; // 此时arr[3].a的值也被改变
arr[0] = 2; // 此时arr1[0]的值不受影响

2.1.2 slice

const arr2 = arr.slice(0);
​
arr2[3].a = 3; // 此时arr[3].a的值也被改变

2.1.3 concat

const arr3 = [].concat(arr);
​
arr3[3].a = 4; // 此时arr[3].a的值也被改变

2.1.4 new Set

const arr4 = [...new Set(arr)];
​
arr4[3].a = 5; // 此时arr[3].a的值也被改变

2.2 对于对象

2.2.1 扩展运算符

const obj = { a: [1, { b: 2 }], c: { d: { e: 1 } } }
const obj1 = {...obj};
​
obj1.a[1].b = 3; // 此时obj.a[1].b的值也会改变
obj1.a = 2; // 此时obj.a的值不受影响

2.2.2 Object.assign

const obj2 = Object.assign({}, obj);obj2.c.d = 5; // 此时obj.c.d的值也会改变

3. 深拷贝

被拷贝引用类型被赋值变量所在的内存空间两个不同的地址修改被拷贝对象所有属性的值的时候不会影响被赋值对象所在空间

3.1 JSON方式实现深拷贝

JSON格式可以支持的数据类型

  • 简单值字符串数值布尔值null
  • 对象类型键值对
  • 数组类型数组的值可以是任意值

JavaScriptJSON格式支持类型有哪些?那么每一个类型被解析前后的变化是如何的?验证一下

const s = 'string';
const num = 123;
const num1 = NaN;
const num2 = Infinity;
const n = null;
const un = undefined;
const fn = function () { };
const o = { a: 1 };
const map = new Map([['123', 123]]);
const list = [1, 2, 3];
const set = new Set([1, 2, 3]);
const sym = Symbol();
const reg = new RegExp('ab+c', 'i');
const date = new Date();
​
const typeData = {
  s,
  // num,
  // num1,
  num2,
  n,
  un,
  fn,
  o,
  map,
  list,
  set,
  sym,
  reg,
  date
}
let jsonType = getTypeMap(typeData);
function getTypeMap(typeData) {
  let result = new Map();
  Object.keys(typeData).forEach(item => {
    const key = Object.prototype.toString.call(typeData[item]);
    const value = JSON.parse(JSON.stringify({ [key]: typeData[item] }));
    result.set(key, value);
  });
  return result
}
​
console.log(jsonType);

如果map中的key对应的value属性或者值丢失,那么就说明JSON不能表示该类型可以自己执行一下看一下结果,是不是对于数字类型的那块有疑问,为什么值是null,可以自己打个断点走调试看看,因为num、num1、num2这三个类型相同他们的Map中的键相同,值发生了替换

JS数据类型JSON格式是否支持解析结果
Number数字 (NaN和Infinity会被转化为null)
String字符串
nullnull
undefined属性消失
Function属性消失
Object对象
Array数组
Date字符串
Map属性存在,值为{}
Set属性存在,值为{}
Symbol属性消失
RegExp属性存在,值为{}

【JSON实现深拷贝】

了解一下JSON.parse() ,是用来解析JSON字符串,参数是一个JSON字符串格式的数据,否则会报错,那么下面的对于格式判断怎么加,有想法可以在评论下告诉我,我白嫖一下。

JSON字符串格式

  • “名称/值”对的集合
  • 值的有序列表
function deepClone(arg) {
  if (arg === null || typeof arg !== 'object' || isDateType(arg)) throw new TypeError('形参必须是一个对象类型或者数组类型的值');
  return JSON.parse(JSON.stringify(arg))
}
​
function isDateType(arg) {
  return Object.prototype.toString.call(arg) === '[object Date]'
}

用上面的typeData的数据测试一下

const result = deepClone(typeData);

console.log(result);
/*
  和上面表中的undefined、函数、Symbol属性消失,NaN和Infinity会被转化为null,Map、Set、RegExp值会消失,值为{}
  {
  s: 'string',
  num: 123,
  num1: null,
  num2: null,
  n: null,
  o: { a: 1 },
  map: {},
  list: [ 1, 2, 3 ],
  set: {},
  reg: {},
  date: '2023-03-15T06:23:29.893Z'
}
*/

3.2 递归实现深拷贝(对象或数组)

通过遍历每一层的数据,如果是基础类型直接赋值,如果是对象类型或者数组类型,就接着递归遍历执行上面的操作。下面自己实现一遍。

function deepClone(arg) {
  const isArray = Array.isArray(arg); // 判断传入参数是不是一个数组
  const result = isArray ? [] : {}; // 设置容器
  // 遍历对象,如果是数组就是当前参数,如果不是数组就是keys
  const rangeData = isArray ? arg :  Object.keys(arg); 
  
​
  rangeData.forEach((item, index) => {
    const flag = isArray ? index : item;
    // 如果rangeData是数组,则当前元素是item,如果rangeData是对象,则当前元素是arg[item]
    const cloneTarget = isArray ? item : arg[item];
    if(isLikeObject(flag, arg)) {
      result[flag] = deepClone(cloneTarget);
    }else {
      result[flag] = cloneTarget;
    }
  })
​
  return result
}
​
function isLikeObject(flag, self) {
  return typeof self[flag] === 'object' && self[flag] !== null
}

测试一下,确实完成了深层拷贝,但是还是有一些问题

const result = deepClone(data);
console.log(result);
/*
  相较于之前JSON实现深拷贝,现在实现了对于NaN、Infinity、undefined的深拷贝,Set、Map、正则、Date会出现数据丢失的情况,对于函数的拷贝引用关系
  
  {
  s: 'string',
  num: 123,
  num1: NaN,
  num2: Infinity,
  n: null,
  un: undefined,
  fn: [Function: fn],
  o: { a: 1 },
  map: {},
  list: [ 1, 2, 3 ],
  set: {},
  sym: Symbol(),
  reg: {},
  date: {}
}
*/

修改一下

function deepClone(arg) {
  if(arg instanceof Date) return new Date(arg);
  if(arg instanceof RegExp) return new RegExp(arg);
  
  const isArray = Array.isArray(arg); // 判断传入参数是不是一个数组
  const result = isArray ? [] : {}; // 设置容器
  // 遍历对象,如果是数组就是当前参数,如果不是数组就是keys
  const rangeData = isArray ? arg :  Object.keys(arg); 
​
  rangeData.forEach((item, index) => {
    const flag = isArray ? index : item;
    // 如果rangeData是数组,则当前元素是item,如果rangeData是对象,则当前元素是arg[item]
    const cloneTarget = isArray ? item : arg[item];
    if(isLikeObject(flag, arg)) {
      result[flag] = deepClone(cloneTarget);
    }else {
      result[flag] = cloneTarget;
    }
  })
​
  return result
}
​
function isLikeObject(flag, self) {
  return typeof self[flag] === 'object' && self[flag] !== null
}

3.3 循环引用问题

关于循环引用的问题,下面是一个循环引用的例子,分别用JSON递归拷贝一下循环引用的对象,会发生什么?

const a = {};
const b = {c: { d: { e: a}}};
a.f = b;

试着用JSON深拷贝拷贝一下,报错类型错误,转换循环结构为JSON

// TypeError: Converting circular structure to JSON

递归深拷贝拷贝一下,范围错误:已超过最大调用堆栈大小

// RangeError: Maximum call stack size exceeded

解决一下循环引用的问题

通过WeakMap的形式,将复制过的变量名存下来,等下次出现直接赋值,现在有三个问题

  • 在哪创建WeakMap
  • 设置什么值
  • 在哪设置WeakMap元素的值

形参初始化WeakMap,在开始保存当前拷贝对象,以当前拷贝对象作为键值,在拷贝对象的时候判断当WeakMap中是否有这个键,有的话,就直接赋值

4. 修改代码,最终实现

function deepClone(arg, hash = new WeakMap()) {
  if(arg instanceof Date) return new Date(arg);
  if(arg instanceof RegExp) return new RegExp(arg);
  
  const isArray = Array.isArray(arg); // 判断传入参数是不是一个数组
  hash.set(arg, isArray ? [...arg] : {...arg});
  const result = isArray ? [] : {}; // 设置容器
  // 遍历对象,如果是数组就是当前参数,如果不是数组就是keys
  const rangeData = isArray ? arg :  Object.keys(arg); 
  
​
  rangeData.forEach((item, index) => {
    const flag = isArray ? index : item;
    // 如果rangeData是数组,则当前元素是item,如果rangeData是对象,则当前元素是arg[item]
    const cloneTarget = isArray ? item : arg[item];
​
    if(isLikeObject(flag, arg)) {
      if(hash.has(cloneTarget)) {
        result[flag] = hash.get(cloneTarget);
      }else {
        result[flag] = deepClone(cloneTarget, hash);
      }
    }else {
      result[flag] = cloneTarget;
    }
  })
  return result
}
​
function isLikeObject(flag, self) {
  return typeof self[flag] === 'object' && self[flag] !== null
}