JavaScript中的深拷贝与浅拷贝(原生structuredClone介绍)

973 阅读5分钟

在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 对象主要包含一个属性:

  1. 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

1723441880018.png 此方法为最新特性,支持情况如下:

  • 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也提供了便捷的深拷贝功能。选择合适的方法取决于实际需求及目标浏览器的兼容性。