实现 javascript 深拷贝,处理 symbol 属性及循环引用

开始

在JavaScript中,浅拷贝和深拷贝是两种复制对象的不同方式,它们在复制对象中嵌套结构时表现出不同的行为和效果。

在这篇文章中你将学到:

  • 浅拷贝和深拷贝的定义
  • js 中常见的浅拷贝及深拷贝
  • JSON 实现深拷贝以及限制
  • 递归实现深拷贝
  • 处理拷贝的symbol属性以及循环引用问题

简单说一下自己浅拷贝和深拷贝的一些理解:

浅拷贝 :它创建一个新的对象,然后将原始对象的属性值复制到新对象中。但是只复制一层,深层次的数据共享内存,改了新的对象,旧的对象也会受到影响。
深拷贝:深拷贝是指创建一个新的对象,该对象包含与原始对象相同的数据,但是在内存中完全独立于原始对象。这意味着修改深拷贝后的对象不会影响原始对象,它们是彼此独立的。

以扩展运算符举例,扩展运算符是一种常见的对象浅拷贝方式

js 复制代码
const foo = {
  a: 1,
  b: 2,
  c: {
    name: "foo",
  },
};

const newFoo = { ...foo };

newFoo.c.name = "bar";

console.log(foo);
console.log(newFoo);

打印:

相比,深拷贝就不会有这样的问题,两个对象是完全独立的,不会影响原对象,可以是使用JSON实现一个简单的深拷贝

js 复制代码
const newFoo = JSON.parse(JSON.stringify(foo));

JS中常见的浅拷贝以及深拷贝

然后再来说说 JS 中常见的深拷贝和浅拷贝。

浅拷贝

浅拷贝再细分点又可以分为数组的浅拷贝对象的浅拷贝

先说数组的浅拷贝:

  • Array.prototype.slice:方法返回一个新的数组对象,这一对象是一个由 startend 决定的原数组

  • Array.prototype.concat:方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。

  • 扩展运算符(...):也是一种数组浅拷贝的方式,经常用来浅拷贝一个数组

js 复制代码
const arr = [1, 3, { name: 3 }, 5];

const copy1 = arr.slice();
const copy2 = [].concat(arr);
const copy3 = [...arr];

对象常见的浅拷贝:

  • 扩展运算符(...):把一个对象展开到另一个对象
  • Object.assign:其实和扩展运算符是差不多的,有了扩展运算符,就不这么用这个了
js 复制代码
const foo = {
  a: 1,
  b: 2,
  c: {
    name: "foo",
  },
};

const newFoo1 = { ...foo };
const newFoo2 = Object.assign({}, foo);

深拷贝

JS 原生实现了一个深拷贝structuredClone(),可以了解下,接收两个参数:

但是有有些限制。就不摊开讲了。

手写实现一个深拷贝

来到重点,手写实现一个深拷贝,面试高频题,也是经典的基础题,不过网上的写法五花八门,很多的写法都有点过时,或者写的很不完整,接下来开始从基础开始一步步实现一个比较完整深拷贝。

JSON.parse(JSON.stringify)

深拷贝实现的两种思路:

  • JSON.parse(JSON.stringify):利用字符串的序列化和反序列化,但是有很多问题和限制,好处就是简单。
  • 递归拷贝

先讲JSON的方案,再说JSON的深拷贝之前,我们简单来了解下JSON:

JSON 是一种轻量数据交换格式,JSON 的数据类型有以下特点:

  • :string,双引号包裹
  • :string、number、boolean、null、数组、普通对象(大括号)
  • 不能写注释,最外面大括号包裹

所以会用JSON做深拷贝对象,会产生以下问题:

  • 数据类型限制 :不能复制、undefinedDateSymbol函数 等等其他的js数据类型,其实也很好理解,因为JSON只支持以上列举的数据类型,其他要么被忽略 要么报错
  • 丢失原型的信息 :最终拷贝的对象原型只会是Object.prototype
  • 不能处理循环引用:对象包含循环引用,JSON.stringify() 将会导致错误。
  • 性能问题 :对于大型复杂对象深度嵌套的对象,容易产生性能问题

使用:

js 复制代码
const foo = {
  a: 1,
  b: 2,
  c: {
    name: "foo",
  },
};

const newObj = JSON.parse(JSON.stringify(foo));

第二种常见的思路就是递归拷贝

先写下网上常见的做法,利用递归加Object.prototype.hasOwnProperty()来实现:

js 复制代码
function deepClone(obj) {
  if (typeof obj !== "object" || obj === null) {
    return obj;
  }

  const res = Array.isArray(obj) ? [] : {};

  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      res[key] = deepClone(obj[key]);
    }
  }

  return res;
}

但是这个实现忽略了很多实现要点,非常残缺,我们可以把实现的要点列举出来:

  1. 我们要拷贝的属性只能是对象自身 的,不能是原型链上的

  2. 拷贝的属性不仅仅是string类型的 ,还有symbol类型的

  3. 对象身上可能会有循环引用,需要处理,而不是陷入死循环

处理Symbol属性

我们实现第二个点,使用Object.getOwnPropertySymbols对象身上的symbol类型属性。

js 复制代码
function deepClone(obj) {
  if (typeof obj !== "object" || obj === null) {
    return obj;
  }

  const res = Array.isArray(obj) ? [] : {};
	
	// 复制 string 类型属性  
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      res[key] = deepClone(obj[key]);
    }
  }

  // 复制 Symbol 类型属性
  const symbols = Object.getOwnPropertySymbols(obj);
  for (const symbolKey of symbols) {
    res[symbolKey] = deepClone(obj[symbolKey]);
  }

  return res;
}

Reflect.ownKeys

但是上面的实现还是不完整。

因为for...in遍历的对象身上可枚举string类型属性,假设我定义了一个不可枚举的属性,那么就遍历不到了 ,定义不可枚举的属性可以通过Object.defineProperty定义(对象的属性默认是可枚举的)。

js 复制代码
Object.defineProperty(foo, "d", {
  enumerable: false, // 属性为不可枚举
  value: "hello",
});

最后上终极大法,用Reflect.ownKeys,这是JavaScript反射 ,不了解反射也关系,会用就行,我们可以查阅mdn来看看这个api到底有什么用。

我们可以把对象的属性就行分类,有常见的以下几种分类方式:

  • 数据类型string类型和symbol类型
  • 可枚举性 :分为可枚举的不可枚举的,我们默认创建的属性就是可枚举的(除了symbol类型)
  • 来源 :对象自身的属性原型链上的属性

这个Reflect.ownKeys简单来说会返回一个数组 ,属性包括:对象自身 的所有属性(包括所有可枚举的不可枚举的stringsymbol类型),不知道能不能听明白。。

然后实现下,真的非常简单,这里需要注意的一点,使用for...of,关于for...infor...of的区别可以看我之前的文章。

js 复制代码
function deepClone(obj) {
  if (typeof obj !== "object" || obj === null) {
    return obj;
  }

  // 判断是否数组还是普通对象
  const res = Array.isArray(obj) ? [] : {};

  // 拿到对象身上所有的属性,返回一个数组
  const keys = Reflect.ownKeys(obj);

  for (const key of keys) {
    res[key] = deepClone(obj[key]);
  }

  return res;
}

处理循环引用

剩下最后一个问题,如何去处理对象身上的循环引用问题。简单说一下js对象身上的循环引用,一般有以下场景:

  • 一个对象间的循环引用
  • 两个对象之间的循环引用
js 复制代码
// 1. 一个对象间的循环引用
const obj1 = {
  a: 1,
};

obj1.b = obj1;

// 2. 两个对象间的循环引用
const foo = {
  x: 1,
};
const bar = {
  y: 2,
};

foo.e = bar;
bar.f = foo;

console.log(obj1);
console.log(foo);
console.log(bar);

在控制台打印的时候,根本翻不完

接下来再说说处理方案:使用ES6新增的WeakMap,中文叫弱映射 。它有一种特性是:对象在外部的引用 消失,在WeakSet中也会消失,被被当成垃圾回收掉。说的挺抽象的,详细的可以自己学学。

最终完整的实现:

js 复制代码
function deepClone(obj, clones = new WeakMap()) {
  // 如果是原始类型或 null,直接返回
  if (typeof obj !== "object" || obj === null) {
    return obj;
  }

  // 检查是否已经克隆过该对象,防止循环引用
  if (clones.has(obj)) {
    return clones.get(obj);
  }

  // 判断是否数组还是普通对象
  const res = Array.isArray(obj) ? [] : {};

  // 将当前对象添加到克隆Map中
  clones.set(obj, res);

  // 拿到所有 key
  const keys = Reflect.ownKeys(obj);

  for (const key of keys) {
    res[key] = deepClone(obj[key], clones);
  }

  return res;
}

// test
const obj = {
  foo: "bar",
  num: 42,
  arr: [1, 2, 3],
  obj: { dd: true },
  [Symbol("symbol属性")]: "hello",
};

// 循环引用
obj.newObj = obj;

const copy = deepClone(obj);
console.log(copy);

到此结束,对你有帮助的话可以点个赞支持下🥰🥰。

相关推荐
浮华似水20 分钟前
简洁之道 - React Hook Form
前端
正小安2 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
小飞猪Jay4 小时前
C++面试速通宝典——13
jvm·c++·面试
_.Switch4 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光4 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   4 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   4 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web4 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常4 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇5 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器