JS对象拷贝的几种实现方法以及如何深拷贝(面试题)

前言

js中的对象深拷贝在项目开发中较常用到,本文介绍一下Js对象拷贝的几种实现方法,以及如何深拷贝。

一、浅拷贝与深拷贝

浅拷贝是创建一个新对象,这个对象有着原始对象属性值的拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的是引用内存地址。可以称之为拷贝了,但没完全拷贝。

深拷贝是对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域来存放新对象,所以修改新对象不会影响原对象。如果不进行深拷贝,其中一个对象改变了浅拷贝所拷贝的引用值内部的属性,就会影响到另一个对象的属性值。

二、几种拷贝方式

1. JSON.parse(JSON.stringify(obj))

复制代码
let a = {
    name: 'Jack',
    age: 18,
    hobbit: ['sing', {type: 'sports', value: 'run'}],
    score: {
        math: 'A',
    },
    run: function() {}, // 无法拷贝
    walk: undefined, // 无法拷贝
    fly: NaN, // --> null
    cy: null,
    date: new Date() // date 字符串
}
let b = JSON.parse(JSON.stringify(a))

此种方式可以深拷贝一个对象,但是这个对象的内容得符合一定的限制要求,才能真正的深拷贝而没有遗漏或拷贝偏差。下面列举这个方法的缺陷。

缺陷:

取不到值为 undefined 的 key;

如果对象里有函数,函数无法被拷贝下来;

无法拷贝 copyObj 对象原型链上的属性和方法;

对象转变为 date 字符串;

NaN 转变为 null。

JSON 本来就不是专门为深拷贝而设计出来的,该方法的原理就是将对象先转换为 JSON 字符串,再从 JSON 字符串解析回 JavaScript 对象。JSON 所能保存的类型有限,转换为 JSON 字符串的过程中会按照 JSON 的一些规则处理,而无法保留原对象的所有细节。所以,如果深拷贝的应用场景无法接受这些细节的丢失,则不要使用这种方式深拷贝。

2. Object.assign(target, source1, source2)

复制代码
var data = {
              a: "123",
              b: 123,
              c: true,
              d: [43, 2],
              e: undefined,
              f: null,
              g: function() {    console.log("g");  },
              h: new Set([3, 2, null]),
              i: Symbol("fsd"),
              k: new Map([    ["name", "张三"],    ["title", "Author"]  ])
        };
var newData = Object.assign({},data)

这种方式一种浅拷贝方法。它只会复制对象的第一层属性,而不会复制对象内部的所有嵌套属性。

Object.assign方法作用是将 targetObj 和 sourceObj 合并,返回值是合并后的 targetObj 的引用,而这个过程只进行了浅拷贝。

3.普通递归函数实现深拷贝

复制代码
function deepClone(source) {
  if (typeof source !== 'object' || source == null) {
    return source;
  }
  const target = Array.isArray(source) ? [] : {};
  for (const key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      if (typeof source[key] === 'object' && source[key] !== null) {
        target[key] = deepClone(source[key]);
      } else {
        target[key] = source[key];
      }
    }
  }
  return target;
}

解决循环引用和symblo类型

复制代码
function cloneDeep(source, hash = new WeakMap()) {
  if (typeof source !== 'object' || source === null) {
    return source;
  }
  if (hash.has(source)) {
    return hash.get(source);
  }
  const target = Array.isArray(source) ? [] : {};
  Reflect.ownKeys(source).forEach(key => {
    const val = source[key];
    if (typeof val === 'object' && val != null) {
      target[key] = cloneDeep(val, hash);
    } else {
      target[key] = val;
    }
  })
  return target;
}

4. 迭代递归方法(解决闭环问题)

复制代码
function deepCopy(data, hash = new WeakMap()) {
  let newData;
  if (typeof data === "object") {
    // null
    if (data === null) {
      newData = data;
    }
    // Array
    else if (Array.isArray(data)) {
      newData = [];
      data.forEach((item) => newData.push(deepClone(item, hash)));
    }
    // Date
    else if (data instanceof Date) {
      newData = new Date(data);
    }
    // regular expression
    else if (data instanceof RegExp) {
      newData = new RegExp(data);
    } else if (data instanceof Set) {
      // 实现set数据的深拷贝
      newData = new Set();
      Array.from(data).forEach((item) => newData.add(deepClone(item, hash)));
    } else if (data instanceof Map) {
      // 实现map数据的深拷贝
      newData = new Map();
      data.forEach((value, key) =>
        newData.set(deepClone(key, hash), deepClone(value, hash))
      );
    }
    // plain object
    else {
      // 用WeakMap的key保存原对象的引用记录, value是对应的深拷贝对象的引用
      // 例如: a:{b:{c:{d: null}}}, d=a, a 的深拷贝对象是 copy, 则 weakmap 里保存一条 a->copy 记录
      // 当递归拷贝到d, 发现d指向a,而a已经存在于weakmap,则让新d指向copy
      if (hash.has(data)) {
        newData = hash.get(data);
      } else {
        newData = {};
        hash.set(data, newData);
        for (let prop in data) {
          newData[prop] = deepClone(data[prop], hash);
        }
      }
    }
  }
  // 基本数据类型
  else {
    newData = data;
  }
  return newData;
}

5.第三方库lodash的cloneDeep()方法

这种方式是否使用,取决于我们项目中是否已使用过lodash其它功能,没必要为了一个深拷贝功能而引入一整个库。

复制代码
import lodash from 'lodash'
let obj = {
    person: {
        name: '张三',
        age: 18,
        hobbies: ['跑步','乒乓球','爬山']
    },
    p: 1
}
const newObj = lodash.cloneDeep(obj)
obj.p = 20
console.log(newObj.p) // 输出1

初次调用deepCopy时,参数会创建一个WeakMap结构的对象,这种数据结构的特点之一是,存储键值对中的健必须是对象类型。

如果待拷贝对象中有属性也为对象时,则将该待拷贝对象存入weakMap中,此时的健是对该待拷贝对象的引用,值是拷贝结果对象的引用。然后递归调用该函数再次进入该函数,传入了上一个待拷贝对象的对象属性的引用和存储了上一个待拷贝对象引用的weakMap,因为如果是循环引用产生的闭环,那么这两个引用是指向相同的对象的,因此会进入if(hash.has())语句内,然后直接赋值return,退出函数,所以不会一直循环递归进栈,以此防止栈溢出。

相关推荐
小徐不徐说2 小时前
序列化与反序列化:把 C++ 对象可靠地保存下来
开发语言·数据库·c++·php
ragnwang2 小时前
Notes: Python pybind11 import C++ module
开发语言·c++·python
Wang's Blog2 小时前
Java 项目实战: 交友项目 - 搜索附近的功能概述和分析
java·开发语言·交友
骑着蜗牛撵大象3272 小时前
大模型 Agent 的沙箱护栏:gVisor 隔离、出口白名单与审计闭环
java·开发语言·jvm·安全·agent沙箱·gvisor·microvm
Dxy12393102163 小时前
JS DES 加密详解
开发语言·javascript·arcgis
充值内卷3 小时前
我给桌面加了个鱼缸:C++ 写后端,WebView2 里养一座 Three.js 小岛
开发语言·javascript·c++
云和数据.ChenGuang4 小时前
深入解析 Python 的 dir():从用法到 CPython 源码实现
开发语言·人工智能·python·langchain
nice_lcj5204 小时前
深入理解 Java 反射:从 Class 对象到方法调用(含性能实测 + 迷你 IOC 实战 + 面试考点)
java·开发语言·后端·面试
椒哥6 小时前
一天被问几十次"允许",我统计了 517 次,然后写了个插件
前端·javascript·github
樱花树下的数学喵6 小时前
手写简易 STL string|C++ 字符串底层实现
开发语言·c++