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,退出函数,所以不会一直循环递归进栈,以此防止栈溢出。

相关推荐
qq_4416857515 分钟前
bash shell笔记——循环结构
开发语言·bash
KAI773825 分钟前
2月11日QT
开发语言·qt
论迹34 分钟前
【JavaEE】-- 多线程(初阶)1
java·开发语言·网络·java-ee
沈清韵34 分钟前
Lisp语言的软件工程
开发语言·后端·golang
录大大i1 小时前
HTML之JavaScript分支结构
前端·javascript·html
呀啊~~1 小时前
【前端框架与库】「React 全面解析」:从 JSX 语法到高阶组件,深度剖析前端开发中的核心概念与最佳实践
前端·javascript·学习·react.js·前端框架
S-X-S1 小时前
Java面试题-Spring Boot
java·开发语言·spring boot
ElseWhereR2 小时前
C++中函数的调用
开发语言·c++
Excuse_lighttime2 小时前
堆排序
java·开发语言·数据结构·算法·排序算法
arong_xu2 小时前
理解C++ Type Traits
开发语言·c++·type_traits