深入理解现代JavaScript的深浅拷贝技术
现代JavaScript高级小册
深入浅出Dart
现代TypeScript高级小册
JavaScript深拷贝与浅拷贝
引言
在JavaScript中,对象的拷贝是一项常见的操作。浅拷贝和深拷贝是两种常用的拷贝方式。浅拷贝只复制对象的引用,而深拷贝创建了一个全新的对象,包含与原始对象相同的值和结构。深拷贝和浅拷贝各有适用的场景和注意事项。本文将详细介绍如何实现一个完整而优雅的深拷贝函数,处理循环引用和特殊类型,优化性能,并探讨深拷贝和浅拷贝的应用场景、注意事项和相关属性。
1. 深拷贝的实现
实现一个完整而优雅的深拷贝函数需要考虑以下几个方面:
1.1 基本类型和特殊类型的处理
在实现深拷贝函数时,首先需要处理基本类型(如字符串、数字、布尔值等)和特殊类型(如函数、正则表达式和日期对象等)。对于基本类型,直接返回其值即可。对于特殊类型,可以选择直接引用原始对象,而不进行复制。
function deepClone(obj) {
// 处理基本类型
if (typeof obj !== 'object' || obj === null) {
return obj;
}
// 处理特殊类型
if (obj instanceof RegExp) {
return new RegExp(obj);
}
if (obj instanceof Date) {
return new Date(obj.getTime());
}
if (obj instanceof Function) {
return obj;
}
// 处理普通对象和数组
const clone = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key]);
}
}
return clone;
}
在上述代码中,我们使用 typeof
操作符判断基本类型,根据对象的类型选择适当的处理方式。对于函数、正则表达式和日期对象,我们使用相应的构造函数创建新的实例。
1.2 处理循环引用
循环引用是指对象属性之间存在相互引用的情况,导致递归复制陷入无限循环。为了处理循环引用,我们可以使用一个额外的数据结构(如 Map
或 WeakMap
)来存储已经复制的对象,以便在遇到循环引用时进行判断和处理。
下面是一个修改后的 deepClone
函数,解决了循环引用问题:
function deepClone(obj, map = new Map()) {
if (typeof obj !== 'object' || obj
=== null) {
return obj;
}
if (map.has(obj)) {
return map.get(obj);
}
const clone = Array.isArray(obj) ? [] : {};
map.set(obj, clone);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key], map);
}
}
return clone;
}
在上述代码中,我们使用 Map
数据结构来存储已经复制的对象。在每次递归调用时,我们首先检查 map
中是否存在当前对象的引用,如果存在则直接返回对应的副本。这样,我们可以避免陷入无限循环。
1.3 性能优化
深拷贝是一项相对耗费性能的操作,特别是在处理大型对象或嵌套层次很深的对象时。为了提高性能,可以考虑以下几个优化策略:
- 循环拷贝:使用循环代替递归,减少函数调用的开销。这可以通过迭代对象的属性并复制它们来实现。
-
使用
JSON
序列化与反序列化:JSON.stringify()
方法可以将对象序列化为字符串,JSON.parse()
方法可以将字符串解析为对象。使用这两个方法可以快速实现深拷贝,但它的适用范围受限,无法处理特殊类型(如函数和正则表达式)和循环引用。 - 使用库函数:许多优秀的 JavaScript 库(如 Lodash、Underscore)提供了高性能的深拷贝函数。这些库经过充分测试和优化,可以满足大多数深拷贝需求。
1.4 完整的深拷贝实现示例
下面是一个完整的深拷贝函数的实现,综合考虑了上述的处理方法:
// 也可以用WeakMap优化
function deepClone(obj, hash = new WeakMap()) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (hash.has(obj)) {
return hash.get(obj);
}
const clone = Array.isArray(obj) ? [] : {};
hash.set(obj, clone);
if (obj instanceof Date) {
return new Date(obj.getTime());
}
if (obj instanceof RegExp) {
const flags = obj.flags;
const pattern = obj.source;
return new RegExp(pattern, flags);
}
if (typeof obj === 'function') {
return cloneFunction(obj);
}
const keys = [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)];
for (const key of keys) {
clone[key] = deepClone(obj[key], hash);
}
return clone;
}
function cloneFunction(func) {
const body = func.toString();
const parameters = body.match(/\((.*?)\)/)[1];
const functionBody = body.substring(body.indexOf('{') + 1, body.lastIndexOf('}'));
return new Function(parameters, functionBody);
}
2. 浅拷贝的实现
与深拷贝不同,浅拷贝只复制对象的引用,而不创建对象的副本。下面是几种常见的浅拷贝方法:
2.1 Object.assign()
Object.assign()
方法用于将所有可枚举属性从一个或多个源对象复制到目标对象,并返回目标对象。它只会复制源对象的属性的引用,而不是属性的值。
const sourceObj = { name:
'John', age: 25 };
const targetObj = Object.assign({}, sourceObj);
console.log(targetObj); // 输出:{ name: 'John', age: 25 }
在上述代码中,我们使用 Object.assign()
方法将源对象的属性复制到目标对象中。targetObj
是 sourceObj
的浅拷贝副本。
2.2 展开语法(Spread Syntax)
展开语法(Spread Syntax)可以用于将一个对象的所有属性展开到另一个对象中。
const sourceObj = { name: 'John', age: 25 };
const targetObj = { ...sourceObj };
console.log(targetObj); // 输出:{ name: 'John', age: 25 }
在上述代码中,我们使用展开语法将源对象的所有属性展开到目标对象中。targetObj
是 sourceObj
的浅拷贝副本。
2.3 数组浅拷贝
对于数组的浅拷贝,可以使用 slice()
或展开语法。
const sourceArray = [1, 2, 3];
const targetArray1 = sourceArray.slice();
const targetArray2 = [...sourceArray];
console.log(targetArray1); // 输出:[1, 2, 3]
console.log(targetArray2); // 输出:[1, 2, 3]
在上述代码中,我们使用 slice()
方法和展开语法将源数组的元素复制到目标数组中。targetArray1
和 targetArray2
都是 sourceArray
的浅拷贝副本。
3. 深拷贝与浅拷贝的应用场景
深拷贝和浅拷贝各有适用的场景:
-
深拷贝的应用场景:
- 当需要创建一个对象的完全独立副本时,以防止对原始对象的修改。
- 在对象状态管理中,需要创建对象的副本以记录历史状态、实现撤销和重做等操作。
- 在数据变换和处理过程中,创建对象的副本以避免对原始数据的修改。
-
浅拷贝的应用场景:
- 当只需要复制对象的引用,而不需要创建对象的副本时。
- 在一些简单的数据处理场景中,浅拷贝可以更高效地完成任务。
4. 注意事项
在使用深拷贝和浅拷贝时,需要注意以下几个问题:
-
循环引用:深拷贝和浅拷贝都需要注意循环引用的问题。循环引用是指对象之间相互引用,导致无限循环。在处理循环引用时,深拷贝需要使用额外的数据结构(如
Map
或WeakMap
)进行记录和判断,而浅拷贝则无法解决循环引用的问题。 - **特殊类型
的处理**:在实现深拷贝和浅拷贝时,需要注意特殊类型的处理。特殊类型包括函数、正则表达式等。对于特殊类型,深拷贝可以选择直接引用原始对象,而浅拷贝只会复制引用。
- 性能开销:深拷贝是一项相对耗费性能的操作,特别是在处理大型对象或嵌套层次很深的对象时。在实际应用中,需要根据场景权衡性能和需求。
结论
深拷贝和浅拷贝是JavaScript中常用的拷贝方式,每种方式都有其适用的场景和注意事项。通过实现一个完整而优雅的深拷贝函数,我们可以轻松地创建对象的独立副本,并处理循环引用和特殊类型。浅拷贝则提供了一种快速复制对象的方式,适用于简单的数据处理场景。根据实际需求和性能要求,选择适合的拷贝方式,可以更好地满足业务需求。
参考资料
- MDN Web Docs: Object.assign()
- MDN Web Docs: Spread Syntax
上一篇: 玩转 JavaScript 拷贝操作
下一篇: JavaScript中的复制技巧大揭秘!
推荐阅读
-
轻松理解并实践JavaScript中的深浅拷贝
-
深入理解现代JavaScript的深浅拷贝技术
-
JavaScript里的对象复制:深入理解深拷贝
-
【Netty】「萌新入门」(七)ByteBuf 的性能优化-堆内存的分配和释放都是由 Java 虚拟机自动管理的,这意味着它们可以快速地被分配和释放,但是也会产生一些开销。 直接内存需要手动分配和释放,因为它由操作系统管理,这使得分配和释放的速度更快,但是也需要更多的系统资源。 另外,直接内存可以映射到本地文件中,这对于需要频繁读写文件的应用程序非常有用。 此外,直接内存还可以避免在使用 NIO 进行网络传输时发生数据拷贝的情况。在使用传统的 I/O 时,数据必须先从文件或网络中读取到堆内存中,然后再从堆内存中复制到直接缓冲区中,最后再通过 SocketChannel 发送到网络中。而使用直接缓冲区时,数据可以直接从文件或网络中读取到直接缓冲区中,并且可以直接从直接缓冲区中发送到网络中,避免了不必要的数据拷贝和内存分配。 通过 ByteBufAllocator.DEFAULT.directBuffer 方法来创建基于直接内存的 ByteBuf: ByteBuf directBuf = ByteBufAllocator.DEFAULT.directBuffer(16); 通过 ByteBufAllocator.DEFAULT.heapBuffer 方法来创建基于堆内存的 ByteBuf: ByteBuf heapBuf = ByteBufAllocator.DEFAULT.heapBuffer(16); 注意: 直接内存是一种特殊的内存分配方式,可以通过在堆外申请内存来避免 JVM 堆内存的限制,从而提高读写性能和降低 GC 压力。但是,直接内存的创建和销毁代价昂贵,因此需要慎重使用。 此外,由于直接内存不受 JVM 垃圾回收的管理,我们需要主动释放这部分内存,否则会造成内存泄漏。通常情况下,可以使用 ByteBuffer.clear 方法来释放直接内存中的数据,或者使用 ByteBuffer.cleaner 方法来手动释放直接内存空间。 测试代码: public static void testCreateByteBuf { ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(16); System.out.println(buf.getClass); ByteBuf heapBuf = ByteBufAllocator.DEFAULT.heapBuffer(16); System.out.println(heapBuf.getClass); ByteBuf directBuf = ByteBufAllocator.DEFAULT.directBuffer(16); System.out.println(directBuf.getClass); } 运行结果: class io.netty.buffer.PooledUnsafeDirectByteBuf class io.netty.buffer.PooledUnsafeHeapByteBuf class io.netty.buffer.PooledUnsafeDirectByteBuf 池化技术 在 Netty 中,池化技术指的是通过对象池来重用已经创建的对象,从而避免了频繁地创建和销毁对象,这种技术可以提高系统的性能和可伸缩性。 通过设置 VM options,来决定池化功能是否开启: -Dio.netty.allocator.type={unpooled|pooled} 在 Netty 4.1 版本以后,非 Android 平台默认启用池化实现,Android 平台启用非池化实现; 这里我们使用非池化功能进行测试,依旧使用的是上面的测试代码 testCreateByteBuf,运行结果如下所示: class io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeDirectByteBuf class io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf class io.netty.buffer.UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeDirectByteBuf 可以看到,ByteBuf 类由 PooledUnsafeDirectByteBuf 变成了 UnpooledUnsafeDirectByteBuf; 在没有池化的情况下,每次使用都需要创建新的 ByteBuf 实例,这个操作会涉及到内存的分配和初始化,如果是直接内存则代价更为昂贵,而且频繁的内存分配也可能导致内存碎片问题,增加 GC 压力。 使用池化技术可以避免频繁内存分配带来的开销,并且重用池中的 ByteBuf 实例,减少了内存占用和内存碎片问题。另外,池化技术还可以采用类似 jemalloc 的内存分配算法,进一步提升分配效率。 在高并发环境下,池化技术的优点更加明显,因为内存的分配和释放都是比较耗时的操作,频繁的内存分配和释放会导致系统性能下降,甚至可能出现内存溢出的风险。使用池化技术可以将内存分配和释放的操作集中到预先分配的池中,从而有效地降低系统的内存开销和风险。 内存释放 当在 Netty 中使用 ByteBuf 来处理数据时,需要特别注意内存回收问题。 Netty 提供了不同类型的 ByteBuf 实现,包括堆内存(JVM 内存)实现 UnpooledHeapByteBuf 和堆外内存(直接内存)实现 UnpooledDirectByteBuf,以及池化技术实现的 PooledByteBuf 及其子类。 UnpooledHeapByteBuf:通过 Java 的垃圾回收机制来自动回收内存; UnpooledDirectByteBuf:由于 JVM 的垃圾回收机制无法管理这些内存,因此需要手动调用 release 方法来释放内存; PooledByteBuf:使用了池化机制,需要更复杂的规则来回收内存; 由于池化技术的特殊性质,释放 PooledByteBuf 对象所使用的内存并不是立即被回收的,而是被放入一个内存池中,待下次分配内存时再次使用。因此,释放 PooledByteBuf 对象的内存可能会延迟到后续的某个时间点。为了避免内存泄漏和占用过多内存,我们需要根据实际情况来设置池化技术的相关参数,以便及时回收内存; Netty 采用了引用计数法来控制 ByteBuf 对象的内存回收,在博文 「源码解析」ByteBuf 的引用计数机制 中将会通过解读源码的形式对 ByteBuf 的引用计数法进行深入理解; 每个 ByteBuf 对象被创建时,都会初始化为1,表示该对象的初始计数为1。 在使用 ByteBuf 对象过程中,如果当前 handler 已经使用完该对象,需要通过调用 release 方法将计数减1,当计数为0时,底层内存会被回收,该对象也就被销毁了。此时即使 ByteBuf 对象还在,其各个方法均无法正常使用。 但是,如果当前 handler 还需要继续使用该对象,可以通过调用 retain 方法将计数加1,这样即使其他 handler 已经调用了 release 方法,该对象的内存仍然不会被回收。这种机制可以有效地避免了内存泄漏和意外访问已经释放的内存的情况。 一般来说,应该尽可能地保证 retain 和 release 方法成对出现,以确保计数正确。
-
深入理解金属切削:现代制造技术的核心原理