手撕深拷贝和浅拷贝

92 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情

前言

本文主要总结一些深拷贝,浅拷贝的方法总结和实现方式,如有写的不准确的地方,欢迎掘友们指出,相互学习,共同进步!在讲述之前建议大家先读一下这篇文章 JS数据类型有哪些,区别是什么?看这篇就够了!

一. 什么是浅拷贝什么是深拷贝?

浅拷贝:

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。

简单来说意思就是:只拷贝该对象的基本数据,该对象的子对象不拷贝

深拷贝:

深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。

简单来说意思就是:会克隆出一个新对象,数据相同,但是引用地址不同

再附上一张图总该懂了吧!

image.png

image.png

二. 浅拷贝实现方式

function isObject(value) {
  const valueType = typeof value
  return (value !== null) && (valueType === "object" || valueType === "function")
}

function ShallowClone(originValue) {
  if (!isObject(originValue)) {
    return originValue
  }
 // 判断传入的对象是数组, 还是对象
  const newObject = Array.isArray(originValue) ? []: {}
  for (const key in originValue) {
    newObject[key] = originValue[key]
  }
  return newObject
}

常用的方法

  • Object.assigin
const newObj = {};
const obj = {
  name: "hi",
  info: {
    age: 18,
    address: {
      city: "南京",
    },
  },
};

Object.assign(newObj, obj);
obj.info.age = 20;
obj.name = "ok";
console.log(newObj, obj);

image.png

  • 扩展运算符(...)
const newObj = {...obj};
  • Array.prototype.concat

数组的 concat 方法其实也是浅拷贝,使用场景比较少,使用concat连接一个含有引用类型的数组时,需要注意修改原数组中的元素的属性,因为它会影响拷贝之后连接的数组

const arr = [1, 2, { name: "hhh" }];
const newArr = arr.concat();
newArr[2].name = "ohmy";
  • Array.prototype.slice
const arr = [1, 2, { name: "hhh" }];
const newArr = arr.slice();

注意:

它不会拷贝对象的继承属性;

它不会拷贝对象的不可枚举的属性;

可以拷贝 Symbol 类型的属性。

三. 深拷贝实现方式

基本实现:

function isObject(value) {
  const valueType = typeof value
  return (value !== null) && (valueType === "object" || valueType === "function")
}

function deepClone(originValue) {
  // 判断传入的originValue是否是一个对象类型
  if (!isObject(originValue)) {
    return originValue
  }

  const newObject = {}
  for (const key in originValue) {
    newObject[key] = deepClone(originValue[key])
  }
  return newObject
}

复杂实现:

function isObject(value) {
  const valueType = typeof value
  return (value !== null) && (valueType === "object" || valueType === "function")
}



function deepClone(originValue, map = new WeakMap()) {
  // 判断是否是一个Set类型
  if (originValue instanceof Set) {
    return new Set([...originValue])
  }

  // 判断是否是一个Map类型
  if (originValue instanceof Map) {
    return new Map([...originValue])
  }

  // 判断如果是Symbol的value, 那么创建一个新的Symbol
  if (typeof originValue === "symbol") {
    return Symbol(originValue.description)
  }

  // 判断如果是函数类型, 那么直接使用同一个函数
  if (typeof originValue === "function") {
    return originValue
  }

  // 判断传入的originValue是否是一个对象类型
  if (!isObject(originValue)) {
    return originValue
  }
  if (map.has(originValue)) {
    return map.get(originValue)
  }

  // 判断传入的对象是数组, 还是对象
  const newObject = Array.isArray(originValue) ? []: {}
  map.set(originValue, newObject)
  for (const key in originValue) {
    newObject[key] = deepClone(originValue[key], map)
  }

  // 对Symbol的key进行特殊的处理
  const symbolKeys = Object.getOwnPropertySymbols(originValue)
  for (const sKey of symbolKeys) {
    // const newSKey = Symbol(sKey.description)
    newObject[sKey] = deepClone(originValue[sKey], map)
  }
  
  return newObject
}

常用的方法

  • 使用JSON.stringify()以及JSON.parse()

    缺点:不可以拷贝 undefined , function, RegExp,Symbol 等等类型,并且如果存在对象的循环引用,也会报错。

//将对象转换为json字符串形式,再将转换而来的字符串转换为原生js对象
const newObj = JSON.parse(JSON.stringify(obj))