JS深拷贝和浅拷贝理解与实现方法

259 阅读4分钟

直接步入正题:

前言

工作中可能会遇到这样一个问题,我明明把数据拷贝了一份,但改变拷贝后的数据居然同时会改变原始数据中的值。那就来看看为什么吧,希望这篇文章可以给你解惑。

什么是浅拷贝

「只拷贝了数据对象的第一层,深层次的数据值与原始数据会互相影响(拷贝后的数据与原始数据还存有关联)」

常见浅拷贝的方式:Object.assign()扩展运算符

const obj1 = { name: 'dog', info: { age: 3 } }
const obj2 = Object.assign({}, obj1)
// 或者
const obj2 = { ...obj1 }

obj2.name = 'cat'
obj2.info.age = 4
console.log(obj1) // { name: 'dog', info: { age: 4 } }
console.log(obj2) // { name: 'cat', info: { age: 4 } }

当拷贝后的对象obj2数据改变的时候会影响原始数据obj1,因为info对象拷贝的是源对象指针。

不清楚指针的可以看前面的这篇文章
JS基础面试高频(一):数据类型

什么是深拷贝

「不管数据对象有多少层,改变拷贝后的值都不会影响原始数据的值。(拷贝后的数据与原始数据毫无关系)」

常见深拷贝的方式:JSON.parse()JSON.stringify()配合使用,这种方式有一个弊端,就是无法正确处理函数 正则 undefined Symbol原因是JSON.stringify时,会先进行一个JSON格式化,获得安全的JSON值,因此如果是非安全的JSON值,就会被丢弃掉。其中undefined、function、symbol这三种类型的值就是非安全的,并且set、map这种数据格式的对象,也并没有被正确处理,而是处理成了一个空对象

const obj1 = { name: 'dog', info: { age: 3 }, fn: function () {} }
const obj2 = JSON.parse(JSON.stringify(obj1))
obj2.name = 'cat'
obj2.info.age = 4
console.log(obj1) // { name: 'dog', info: { age: 3 }, fn: function(){} }
console.log(obj2) // { name: 'cat', info: { age: 4 } }

我们也可以手写一个深拷贝的方法(简易基础版)

function deepClone(source) {
  // null数据需要特殊处理
  if (source === null) {
    return source
  }
  // 校验要拷贝的数据是对象还是数组
  const target = Array.isArray(source) ? [] : {}
  for (const k in source) {
    const val = source[k]
    const valueType = typeof val
    // 校验拷贝的数据类型
    if (valueType === 'function') {
      target[k] = new Function(`return ${val.toString()}`)()
    } else if (valueType === 'object') {
      target[k] = deepClone(val)
    } else {
      target[k] = val
    }
  }
  return target
}

const obj1 = { name: 'dog', info: { age: 3 }, fn: function () {} }
const obj2 = deepClone(obj1)
obj2.name = 'cat'
obj2.info.age = 4
console.log(obj1) // { name: 'dog', info: { age: 3 }, fn: function(){} }
console.log(obj2) // { name: 'cat', info: { age: 4 }, fn: function(){} } 

当然还可以使用第三方库Lodash中的_.cloneDeep(value),强烈推荐这个库,功能非常强大!

实现方法

提到深拷贝和浅拷贝,就要涉及到两个知识点,一个是基本是据类型和引用数据类型,一个是堆内存和栈内存

浅拷贝

浅拷贝实现的方式

let obj2 = obj1;

let obj2 = Object.assign({}, obj1);

let arr2 = [...arr1];或者let obj2 = {..obj1};

let arr2 = [].concat.(arr1);

let arr2 = arr1.slice();

let arr2 = Array.from(arr1);

数组还有其他的方法map(),reduce(),filter()都能实现浅拷贝

深拷贝

深拷贝实现的方法

let obj2 = JSON.parse(JSON.stringify(obj1))

原理:基本类型拷贝是直接在栈内存新开空间,直接复制一份名-值,两者互不影响。而引用数据类型,比如对象,变量名在栈内存,值在堆内存,拷贝只是拷贝了堆内存提供的指向值的地址,而JSON.stringify()巧就巧在能将一个对象转换成字符串,也就是基本类型,那这里的原理就是先利用JSON.stringify()将对象转变成基本数据类型,然后使用了基本类型的拷贝方式,再利用JSON.parse()将这个字符串还原成一个对象,达到了深拷贝的目的。

递归的方法

// 深拷贝
function deepClone(data, hash = new WeakMap()) {
  if (typeof data !== "object" || data === null) {
    return data;
  }

  if (hash.has(data)) {
    return hash.get(data);
  }

  let newData;

  if (Array.isArray(data)) {
    newData = [];
  } else if (data instanceof Date) {
    return new Date(data);
  } else if (data instanceof RegExp) {
    const clonedRegExp = new RegExp(data.source, data.flags);
    if (data.global || data.sticky) {
      clonedRegExp.lastIndex = data.lastIndex;
    }
    return clonedRegExp;
  } else if (data instanceof Set) {
    newData = new Set();
    data.forEach((item) => newData.add(deepClone(item, hash)));
  } else if (data instanceof Map) {
    newData = new Map();
    data.forEach((value, key) => newData.set(key, deepClone(value, hash)));
  } else if (data instanceof WeakMap || data instanceof WeakSet) {
    return data; // 无法克隆 WeakMap/WeakSet,直接返回原对象
  } else if (typeof data === "function") {
    const clonedFunc = function (...args) {
      return data.apply(this, args);
    };
    Object.defineProperties(clonedFunc, Object.getOwnPropertyDescriptors(data));
    return clonedFunc;
  } else {
    const descriptors = Object.getOwnPropertyDescriptors(data);
    const proto = Object.getPrototypeOf(data);
    newData = proto
      ? Object.create(proto, descriptors)
      : Object.create(Object.prototype);
  }

  hash.set(data, newData);

  if (Array.isArray(data)) {
    data.forEach((item, index) => {
      newData[index] = deepClone(item, hash);
    });
  } else {
    Reflect.ownKeys(data).forEach((key) => {
      const descriptor = Object.getOwnPropertyDescriptor(data, key);
      if (descriptor.get || descriptor.set) {
        Object.defineProperty(newData, key, descriptor);
      } else {
        newData[key] = deepClone(data[key], hash);
      }
    });
  }

  // 处理 Map/Set 里的 Symbol 属性
  if (data instanceof Map || data instanceof Set) {
    Reflect.ownKeys(data).forEach((symKey) => {
      if (typeof symKey === "symbol") {
        newData[symKey] = deepClone(data[symKey], hash);
      }
    });
  }

  return newData;
}