开始
在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:方法返回一个新的数组对象,这一对象是一个由
start
和end
决定的原数组 -
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(),可以了解下,接收两个参数:
- value:被克隆的对象。可以是任何结构化克隆支持的类型。
- transfer(可选):是一个可转移对象的数组,里面的
值
并没有被克隆,而是被转移到被拷贝对象上。
但是有有些限制。就不摊开讲了。
手写实现一个深拷贝
来到重点,手写实现一个深拷贝,面试高频题,也是经典的基础题,不过网上的写法五花八门,很多的写法都有点过时,或者写的很不完整,接下来开始从基础开始一步步实现一个比较完整深拷贝。
JSON.parse(JSON.stringify)
深拷贝实现的两种思路:
- JSON.parse(JSON.stringify):利用字符串的序列化和反序列化,但是有很多问题和限制,好处就是简单。
- 递归拷贝
先讲JSON的方案,再说JSON的深拷贝之前,我们简单来了解下JSON:
JSON 是一种轻量 的数据交换格式,JSON 的数据类型有以下特点:
- 键 :string,双引号包裹。
- 值:string、number、boolean、null、数组、普通对象(大括号)
- 不能写注释,最外面大括号包裹。
所以会用JSON做深拷贝对象,会产生以下问题:
- 数据类型限制 :不能复制、undefined 、Date 、Symbol 、函数 等等其他的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;
}
但是这个实现忽略了很多实现要点,非常残缺,我们可以把实现的要点列举出来:
-
我们要拷贝的属性只能是对象自身 的,不能是原型链上的
-
拷贝的属性不仅仅是
string
类型的键 ,还有symbol
类型的键 -
对象身上可能会有循环引用,需要处理,而不是陷入死循环
处理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
简单来说会返回一个数组 ,属性包括:对象自身 的所有属性(包括所有可枚举的 和不可枚举的 的string 和symbol类型),不知道能不能听明白。。
然后实现下,真的非常简单,这里需要注意的一点,使用for...of
,关于for...in
和for...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);
到此结束,对你有帮助的话可以点个赞支持下🥰🥰。