在JavaScript编程中,拷贝对象是一个非常常见的需求。拷贝方式主要分为两种:浅拷贝和深拷贝。本文将详细介绍这两种拷贝方式的概念、浏览器最新版本中的实现方法及其优劣、兼容性问题,以及常用的三方库实现。
1. 深拷贝与浅拷贝的概念
1.1 浅拷贝
浅拷贝仅复制对象的第一层属性。对于复杂(嵌套)对象,浅拷贝仅复制对象的引用,而不是实际内容。例如,Object.assign和数组的slice方法都是浅拷贝。
1.2 深拷贝
深拷贝则是复制对象的所有层级的属性,不管是基本类型还是复杂类型,目标对象都与源对象完全独立。这意味着修改目标对象不会影响源对象,反之亦然。
2. 实现方法及其优劣
2.1 浅拷贝方法
2.1.1 Object.assign
let obj = {a: 1, b: {c: 2}};
let shallowCopy = Object.assign({}, obj);
优点:
- 语法简单,易于使用。
缺点:
- 仅能实现浅拷贝,不适用于深层嵌套对象。
2.1.2 展开运算符 (...)
let obj = {a: 1, b: {c: 2}};
let shallowCopy = {...obj};
优点:
- 与Object.assign类似,语法更加简洁。
缺点:
- 同样仅能实现浅拷贝。
2.2 深拷贝方法
2.2.1 JSON 的 stringify 和 parse
let obj = {a: 1, b: {c: 2}};
let deepCopy = JSON.parse(JSON.stringify(obj));
优点:
- 语法简单,适用于大多数场景,适用低版本浏览器。
缺点:
- 只能深拷贝可以被 JSON 序列化的类型。不能处理函数、undefined、Symbol、Date、RegExp、Map、Set、Blob、File、ArrayBuffer等。对于包含循环引用的对象,会抛出错误。
- 转换过程中会丢失类型信息,Date 会被转换为字符串,Map 和 Set 会被转换为普通对象和数组。
- 由于需要先将对象转换为字符串,再解析回对象,性能相对较低,尤其是在对象较大或较复杂时。
2.2.2 structuredClone(obj, options)(最新浏览器特性)
基本使用
//基本对象的深拷贝:
const obj = { a: 1, b: { c: 2 } };
const clone = structuredClone(obj);
console.log(clone); // { a: 1, b: { c: 2 } }
console.log(clone.b !== obj.b); // true,b 是不同的对象
//数组的深拷贝
const arr = [1, 2, [3, 4]];
const cloneArr = structuredClone(arr);
console.log(cloneArr); // [1, 2, [3, 4]]
console.log(cloneArr[2] !== arr[2]); // true,内部数组是不同的引用
//日期对象的深拷贝:
const date = new Date();
const cloneDate = structuredClone(date);
console.log(cloneDate); // 输出与原日期相同的日期
console.log(cloneDate instanceof Date); // true
//Map 和 Set 的深拷贝:
const map = new Map([[1, 'one'], [2, 'two']]);
const cloneMap = structuredClone(map);
console.log(cloneMap); // Map(2) { 1 => 'one', 2 => 'two' }
console.log(cloneMap !== map); // true,map 是不同的引用
const set = new Set([1, 2, 3]);
const cloneSet = structuredClone(set);
console.log(cloneSet); // Set(3) { 1, 2, 3 }
console.log(cloneSet !== set); // true,set 是不同的引用
参数options
structuredClone(obj, options)
方法的第二个参数 options
是一个可选参数,用于自定义克隆过程。这个参数允许你指定如何处理某些特殊类型的对象。目前,options
对象主要包含一个属性:
transfer
: 这是一个数组,用于列出应该被转移而不是克隆的对象。
这里是 transfer
选项的主要用途:
- 对于可转移对象(如 ArrayBuffer, MessagePort 等),你可以使用
transfer
选项来指示应该转移这些对象的所有权,而不是克隆它们。 - 转移比克隆更高效,因为它只是将对象的所有权从一个上下文转移到另一个上下文,而不是创建一个完整的副本。
- 转移后,原始对象在源上下文中将变为不可用。
const buffer = new ArrayBuffer(1024);
const clone = structuredClone(buffer, { transfer: [buffer] });
// 此时,原始的 buffer 变为不可用
console.log(buffer.byteLength); // 0
console.log(clone.byteLength); // 1024
需要注意的是,structuredClone()
方法和它的 options
参数仍然相对较新,未来可能会添加更多的选项。
优点:
- 可以处理丰富的数据类型,包括对象、数组、日期、Map、Set、Blob、File、ArrayBuffer、TypedArray等,还能处理嵌套结构和循环引用。
- 保留了对象的类型信息,例如,Date 仍然是 Date 对象,Map 仍然是 Map 对象,等等
- 不能复制某些类型的对象,如函数、DOM 节点、WeakMap 和 WeakSet,它会抛出错误。
- 通常在处理复杂对象时性能更好,因为它是原生实现的,优化了许多底层细节。
缺点:
- 仅支持最新的浏览器。
上述方法无法对函数进行克隆,那么《如何克隆一个函数》呢?
3. 浏览器兼容性
3.1 Object.assign 和 展开运算符 (...)
这些方法均在现代浏览器中广泛支持,包括Chrome、Firefox、Safari等。
3.2 JSON.stringify 和 JSON.parse
这两个方法在几乎所有现代与较旧版本的浏览器中均有良好的支持。
3.3 structuredClone
此方法为最新特性,支持情况如下:
- Chrome: 98+
- Firefox: 94+
- Safari: 15.4+
由于这是新特性,旧版本浏览器可能不支持。
4. 常用三方库实现
对于更加复杂的对象拷贝需求,可以考虑使用成熟的三方库,如Lodash、Underscore等。以下是两者的示例:
4.1 Lodash
安装Lodash:
npm install lodash
使用Lodash的_.cloneDeep
:
let _ = require('lodash');
let obj = {a: 1, b: {c: 2}};
let deepCopy = _.cloneDeep(obj);
4.2 Underscore
安装Underscore:
npm install underscore
使用Underscore的_.clone
:
let _ = require('underscore');
let obj = {a: 1, b: {c: 2}};
let shallowCopy = _.clone(obj, true); // 使用true参数实现深拷贝
总结
在JavaScript中,不同的拷贝方法有着不同的适用场景。浅拷贝适用于对象结构简单的场景,而深拷贝则适用于复杂嵌套对象。在现代浏览器环境下,structuredClone
是一个强大的新特性,能够处理各种复杂数据结构。在此之外,成熟的三方库如Lodash也提供了便捷的深拷贝功能。选择合适的方法取决于实际需求及目标浏览器的兼容性。