一 深浅拷贝的概念
深拷贝和浅拷贝其实针对的是引用数据类型 : 浅拷贝就是只拷贝一层, 深拷贝就是层层拷贝
- 浅拷贝的基本概念就是: 将我们原对象的引用地址 , 直接赋值给新对象。此时新旧对象是共享同一块内存的。也就是说 , 当更新新旧对象中的属性值时 , 更新的其实是同一块内存中的值。
- 深拷贝的基本概念是: 创建一个新的对象 , 将原对象所有属性的值拷贝过来 , 并且是具体的值 , 而不是引用地址。这样子新对象的更改就不会影响到原对象。
浅拷贝 | 仅拷贝对象的引用地址, 或者是对象属性的值 |
---|---|
深拷贝 | 会遍历对象, 并返回一个指向新引用地址的对象 |
二 实现深拷贝的方法
1.使用JSON序列化和反序列化
这种方法最简单直接 , 但有一些局限性 , 比如无法处理函数 , undefined , Symbol , 循环引用等
function deepCopy(obj) {
return JSON.parse(JSON.stringify(obj));
}
// 测试
const original={
name:'zhangsan',
age:20,
skills:['js','css','html'],
address:{
city:'beijing',
country:'china'
},
[Symbol('sex')]:'男'
}
const copy = deepCopy(original);
console.log(copy);
console.log(copy === original); // 输出: false
console.log(copy.address === original.address); // 输出: false
2.使用递归
递归是实现深拷贝的常用方法 , 通过递归遍历对象的每个属性 , 逐层赋值。
// 定义一个深拷贝函数,使用WeakMap来跟踪已经拷贝过的对象,防止循环引用导致无限递归
function deepCopy(obj, hash = new WeakMap()) {
// 如果对象是正则表达式,直接返回一个新的正则表达式实例, 其内容与 obj 相同。 (下列类似)
if (obj instanceof RegExp) return new RegExp(obj);
if (obj instanceof Date) return new Date(obj);
if (obj instanceof Function) return new Function('return ' + obj.toString())();
// 如果对象不是对象类型或者为null,直接返回该值(基本数据类型)
if (typeof obj !== 'object' || obj === null) return obj;
// 如果对象已经在hash表中,说明已经拷贝过,直接返回拷贝的对象
if (hash.has(obj)) return hash.get(obj);
// 使用对象的构造函数创建一个新的空对象作为拷贝的目标
let cloneObj = new obj.constructor();
// 将原对象和拷贝的对象存入hash表,用于后续查找是否已经拷贝
hash.set(obj, cloneObj);
// 遍历原对象的所有可枚举属性
for (let key in obj) {
// 确保属性是对象自身的属性,而不是继承的属性
if (obj.hasOwnProperty(key)) {
// 递归调用deepCopy函数,拷贝每个属性到新的对象中
cloneObj[key] = deepCopy(obj[key], hash);
}
}
// 返回拷贝后的对象
return cloneObj;
}
// 测试代码
const original = {
name: 'zhangsan',
age: 20,
skills: ['js', 'css', 'html'],
address: {
city: 'beijing',
country: 'china'
},
// 使用Symbol创建一个唯一属性
[Symbol('sex')]: '男'
};
// 设置一个循环引用
original.self = original;
const copy = deepCopy(original);
// 打印原对象和拷贝对象,查看它们的内容
console.log(original);
console.log(copy);
// 检查原对象和拷贝对象是否相同(引用比较),预期输出为false
console.log(copy === original);
// 检查拷贝对象的address属性是否与原对象的address属性相同(引用比较),预期输出为false
console.log(copy.address === original.address);
关键代码详解:
-
function deepCopy(obj,hash=new WeakMap()):
deepCopy 函数接受两个参数:obj
是要复制的对象,hash
是一个默认参数,是一个新的WeakMap
对象。WeakMap
是一种集合类型,其键是对象,而值是任意值。在这里,hash
用于存储已经复制过的对象,以避免循环引用导致的问题。 -
if(obj instanceof Function) return new Function('return '+obj.toString())()
:
如果obj
是一个函数,通过将其转换为字符串形式,然后使用new Function
构造函数重新创建一个新的函数。这种方法有一定的局限性,因为它不会复制函数的上下文或闭包。 -
if(hash.has(obj)) return hash.get(obj)
:
检查hash
中是否已经存储了obj
的副本。如果是,则直接返回该副本,以避免循环引用和重复复制。 -
let cloneObj=new obj.constructor()
:
使用obj
的构造函数创建一个新的空对象cloneObj
。这样做是为了确保新对象的类型与obj
相同。 -
hash.set(obj,cloneObj)
:
将obj
和它的副本cloneObj
存储在hash
中,以便后续可以快速查找副本。 -
for(let key in obj): 遍历
obj` 的所有可枚举属性。 -
if(obj.hasOwnProperty(key)){
:
检查属性是否是obj
自身的属性,而不是继承自原型链的。 -
cloneObj[key]=deepCopy(obj[key],hash)
:
递归调用deepCopy
函数,将obj
的每个属性值也进行深度复制,并将复制后的值赋给cloneObj
的相应属性。
总之,这个函数通过递归地复制对象的所有属性(包括嵌套对象和特定类型的对象,如正则表达式、日期和函数),实现了一个较为完善的深度复制功能。同时,通过使用 WeakMap
来避免循环引用,提高了复制过程的效率和安全性。
三 知识扩展 (也许基础不够扎实的同学都会有以下疑问)
1.为什么下列代码中需要
let cloneObj=new obj.constructor()
?
答:
在这段代码中,let cloneObj = new obj.constructor();
这一行是用来创建一个新的空对象,这个新对象将作为原对象 obj
的深拷贝的目标。使用 obj.constructor
是为了获取原对象的构造函数,然后通过 new
关键字来创建一个新的实例。
这种方法的好处是它能够处理各种不同类型的对象,包括用户自定义的对象。因为 constructor
属性指向了创建该对象实例的构造函数,所以使用 new obj.constructor()
可以创建一个与 obj
相同类型的新对象。这对于深拷贝来说是必要的,因为我们需要一个与原对象结构相同的新对象来存储拷贝后的属性。
然而,需要注意的是,这种方法并不总是完美的。例如,如果原对象的构造函数有复杂的逻辑(比如依赖于特定的参数或执行了某些初始化操作),那么简单地使用 new obj.constructor()
可能不会得到预期的结果。但在大多数情况下,对于简单的对象或那些没有特殊构造函数逻辑的对象来说,这种方法是有效的。
总的来说,let cloneObj = new obj.constructor();
这行代码在深拷贝函数中起到了创建一个与原对象相同类型的新对象的作用,这是深拷贝过程中不可或缺的一步。
- 为什么需要用
if(obj.hasOwnProperty(key))
来检查属性是否是 obj 自身的属性,而不是继承自原型链的?
答:
在JavaScript中,对象的属性可以是自身定义的,也可以是从其原型链上继承而来的。当你使用for...in
循环遍历一个对象的属性时,循环会枚举对象自身的所有可枚举属性,以及它从原型链上继承的可枚举属性。
在你的deepCopy
函数中,目标是创建一个与原始对象obj
结构相同但完全独立的新对象cloneObj
。这意味着你只关心复制obj
自身的属性,而不关心它从原型链上继承的属性。原因如下:
(1) 保持对象的一致性:如果复制了继承的属性,那么新对象cloneObj
可能会包含一些不属于原始对象obj
逻辑结构的属性。这可能会导致cloneObj
的行为与obj
不完全一致。
(2) 避免无限循环:在某些情况下,如果原型链上的某个对象也包含了指向原始对象obj
的引用(例如,通过循环引用),并且没有使用hash
来跟踪已经复制的对象,那么复制过程可能会陷入无限循环。虽然你的函数已经通过hash
来避免了这种情况,但复制继承的属性仍然是不必要的。
(3) 性能考虑:虽然在这个特定的函数中性能差异可能不大,但避免复制不必要的属性总是一个好习惯,特别是在处理大型对象或复杂对象图时。
obj.hasOwnProperty(key)
方法用于检查对象obj
自身是否具有名为key
的属性,而不会检查其原型链。这确保了只有obj
自身的属性才会被复制到cloneObj
中。
因此,在for...in
循环中使用if(obj.hasOwnProperty(key))
是一个好的实践,它确保了只复制对象自身的属性,而忽略从原型链上继承的属性。这有助于保持复制过程的一致性和效率。
- 什么是WeakMap?
答:
WeakMap
是 JavaScript 中的一个集合类型,它允许你将对象类型(Object)的值(称作“键”)映射到任意值(称作“值”)。与普通的 Map
不同,WeakMap
中的键是“弱引用”的,这意味着 WeakMap
不会阻止垃圾回收器回收其键所指向的对象。如果没有其他引用指向某个对象,并且该对象仅作为 WeakMap
的键存在,那么该对象可以被垃圾回收器回收,此时其对应的键值对也会从 WeakMap
中消失。
WeakMap
的特点包括:
(1) 键必须是对象:WeakMap
的键只能是对象,不能是原始值(如字符串、数字、布尔值、undefined
或 null
)。如果尝试使用原始值作为键,会抛出 TypeError
。
(2) 键是弱引用:由于 WeakMap
的键是弱引用的,因此不会阻止垃圾回收器回收键所指向的对象。这使得 WeakMap
特别适合用于缓存或存储私有数据,因为当对象不再需要时,它们可以自动被垃圾回收,而不需要手动删除 WeakMap
中的条目。
(3) 不可枚举:WeakMap
没有迭代方法(如 keys()
、values()
、entries()
或 forEach()
),也不能被 for...in
循环或 Object.keys
等方法遍历。这是因为 WeakMap
的设计初衷是提供一种不暴露其内容的集合类型。
(4) 没有 clear()
方法:WeakMap
没有提供 clear()
方法来清空所有键值对。这是因为 WeakMap
的设计是基于弱引用的,一旦对象不再被引用,它们就会被垃圾回收,因此不需要手动清空。
(5) 私有属性存储:由于 WeakMap
的键是弱引用的,并且不可枚举,因此它可以用作在对象上存储私有属性的机制。通过将对象作为键,并将私有数据作为值存储在 WeakMap
中,可以实现私有属性的访问,而不会污染对象的公共接口。
(6) 性能优化:由于 WeakMap
的键是弱引用的,因此它们不会增加对象的引用计数,这有助于减少内存泄漏的风险,并可能提高垃圾回收器的性能。
总之,WeakMap
是一种非常有用的集合类型,特别适用于需要存储与对象相关联的数据但不希望影响垃圾回收的场景。例如: 在闭包中用WeakMap()
就可以赋值避免内存泄漏的问题。
如果有不懂闭包和内存泄漏问题的, 可以去看我前段时间发的一篇文章:
前端JavaScript面试重难点: 闭包+内存泄漏+垃圾回收机制