关于JS的深拷贝,你用对方法了吗?

b57b6b85a04a211c32715563ac54c2ff.jpeg

JS深拷贝,你用对了吗?

73bf2fe71f6aaad0643bd6ebea8b66ab.jpeg

在JavaScript中,深拷贝一个对象是创建一个全新的对象,包括嵌套对象在内,所有属性都是完全独立的副本。这与浅拷贝不同,浅拷贝只会复制第一级属性,而嵌套的对象则是引用,而非复制。

在JavaScript中,有多种方法可以进行深拷贝,但是你需要结合使用场景选择最佳的。

能否使用 JSON.parse & JSON.stringify 吗? ❌

JSON.parse(JSON.stringify(obj))是一种使用JSON序列化创建对象的深度拷贝的方法。(它适用于不包含任何循环引用的对象,但如果对象包含循环引用,则该方法会失败)。

例如这个示例:

const obj = { a: 1, b: { c: 2 } };
const clone = JSON.parse(JSON.stringify(obj));
console.log(clone);
// Output: { a: 1, b: { c: 2 } }

实际上,这种方法很棒,而且性能出乎意料地好,但是它存在一个问题,无法很好地解决某些情况。

以这个为例:

const calendarEvent = {
  title: "source: https://www.qianduandaren.com",
  date: new Date(123),
  attendees: ["Lian"]
}
// JSON.stringify将date转换为字符串
const copy = JSON.parse(JSON.stringify(calendarEvent))

如果我们打印输出copy变量,将会得到:

{
  title: "source: https://www.qianduandaren.com",
  date: "1970-01-01T00:00:00.123Z"
  attendees: ["Lian"]
}

这不是我们想要的!日期应该是一个Date对象,而不是字符串。

能否使用 Lodash 或 underscore _.cloneDeep() 的深拷贝方法? ❌

Lodash或underscore _.cloneDeep()方法 - 这些库提供了一个深拷贝函数,可以处理循环引用和其他边缘情况。

例如:

const _ = require('lodash');
const obj = { a: 1, b: { c: 2 } };
const clone = _.cloneDeep(obj);
console.log(clone);
// 输出:{ a: 1, b: { c: 2 } }

到目前为止,Lodash的cloneDeep函数一直是解决这个问题的常见方法。

实际上,这种方法确实可以正常工作:

import cloneDeep from 'lodash/cloneDeep'
const calendarEvent = {
title: "源自:https://www.qianduandaren.com/",
date: new Date(123),
attendees: ["Lian"]
}
// ✅ 一切正常!
const clonedEvent = cloneDeep(calendarEvent)

然而,有一个问题。我的开发环境中使用的 Import 导入,每个导入模块的大小(以千字节为单位)。而这个特定的函数相当的庞大,经过最小化处理后的大小为 17.4kb,经过压缩后的大小为 5.3kb。

e2da8848e19aba41ce5adc3eecab9256.jpeg

这个大小估算仅基于单独导入函数。如果您选择常见的导入方式,暂不考虑 tree shaking(可能不总是有效)那么整个引入函数包,可能会带来高达 25kb 的数据量。

2bdcb76d0d1b71e27a3f2cc5320a73d5.jpeg

尽管这个数据量不算大,但它在我们的情况下是不必要的,因为现代浏览器已经内置了 Structured Clone 功能,后面会原生实现这个功能。

延伸阅读:什么是Tree shaking?

"Tree shaking" 是一个 JavaScript 打包工具优化代码大小的技术。它的主要目的是消除应用程序中未使用的代码。通过静态代码分析,tree shaking 可以识别和删除不需要的代码,从而减小生成的 bundle 大小,提高网页的性能和加载速度。

然而,tree shaking 并不总是有效的,特别是在处理动态模块引入、样式和模板等场景时,很难在构建时知道具体要引入哪些模块。此时,tree shaking 可能无法识别未使用的代码,从而无法起到优化代码大小的效果。因此,即使 tree shaking 可能无效,有时也需要考虑代码的大小。

延伸阅读:能深拷贝所有的对象吗?存在其他问题吗?

虽然 Lodash 和 Underscore 库都提供了 _.cloneDeep() 方法进行深拷贝,但这些库也存在一些缺点。

首先,这些库是第三方库,需要进行安装和引入。如果项目本身并没有使用这些库,那么引入它们只是为了进行深拷贝可能会增加项目的体积。

其次,这些库虽然可以解决大多数的深拷贝问题,但仍然无法复制一些特定的对象,如 Blob、File、ImageData 和 AudioContext 等。

最后,这些库在处理大型、复杂的对象时可能会导致性能问题,因为它们需要递归地遍历整个对象,并复制每个属性和嵌套对象。在处理大型对象时,这可能会导致严重的性能问题。

因此,对于需要深拷贝特定类型对象的情况,应该寻找其他更适合的解决方案。

能否使用展开运算符来深度克隆一个对象吗?❌

不行,JavaScript中的展开运算符(...)只能对对象进行浅拷贝(shallow clone)。使用展开运算符克隆一个对象时,任何嵌套的对象仍将被引用而不是被复制,因此在克隆对象中对嵌套对象进行的更改也会影响原始对象。

以下是一个例子:

const original = {a: 1, b: {c: 2}};
const clone = {...original};

console.log(clone);
// Output: {a: 1, b: {c: 2}}

clone.b.c = 3;

console.log(original);
// Output: {a: 1, b: {c: 3}}

在这个例子中,即使克隆对象是一个新对象,对克隆的b属性所做的任何更改也会影响原始对象的b属性。

可以使用 Object.assign 在 javascript 中深度克隆一个对象吗?❌

不行,JavaScript 中的 Object.assign() 方法只会执行浅克隆(shallow clone)一个对象。当使用 Object.assign() 来克隆一个对象时,任何嵌套的对象仍然会被引用而不是被复制,因此克隆对象中嵌套对象的更改也会影响到原始对象。

以下是一个示例:

const original = {a: 1, b: {c: 2}};
const clone = Object.assign({}, original);

console.log(clone);
// Output: {a: 1, b: {c: 2}}

clone.b.c = 3;

console.log(original);
// Output: {a: 1, b: {c: 3}}

在这个例子中,尽管克隆对象是一个新的对象,但是对克隆对象的 b 属性进行的任何更改也会影响到原始对象的 b 属性。

结构化克隆(Structured Clone)✅

结构化克隆是 JavaScript 中的一个概念,它指的是创建对象的深层克隆过程,包括其嵌套对象、数组以及其他复杂数据结构。结构化克隆算法被 JavaScript 中多个 API 所使用,例如用于窗口对象(例如页面和 iframe 之间)之间通信的 postMessage 方法,用于在浏览器中存储和检索数据的 IndexedDB API,以及用于后台处理网络请求的 Service Workers API。

结构化克隆算法通过递归地复制对象的所有属性,包括嵌套对象和数组,将其复制到具有相同结构和值的新对象中。它可以处理多种数据类型,包括函数、日期、正则表达式、数组和对象。

结构化克隆算法是在 JavaScript 中创建复杂数据结构的深层克隆的强大工具,并经常用于在浏览器或其他 JavaScript 环境中执行复杂的数据操作。

1、手写代码实现

使用递归自定义实现 —— 这种方法涉及定义一个使用递归创建对象的深层克隆的函数。

const original = { a: 1, b: { c: new Date() } };

const deepClone = (obj) => {
  if (obj === null || typeof obj !== "object") return obj;
  let clone = Array.isArray(obj) ? [] : {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] =
        obj[key] instanceof Date
          ? new Date(obj[key].getTime())
          : deepClone(obj[key]);
    }
  }
  return clone;
};

const clone = deepClone(original);

console.log(clone);
// Output: {a: 1, b: {c: 2022-12-31T08:00:00.000Z}}

clone.b.c.setFullYear(2023);

console.log(original);
// Output: {a: 1, b: {c: 2022-12-31T08:00:00.000Z}}

此方法可以解决上述所有问题。以下是上述代码解读:

首先,我们定义了一个原始对象 original,它包含两个属性 a 和 b,其中 b 属性的值是一个日期对象。

const original = { a: 1, b: { c: new Date() } };

接下来,定义了一个 deepClone 函数,用于对任意对象进行深拷贝。这个函数首先检查传入的对象是否是 null 或非对象,如果是,则直接返回该对象。

const deepClone = (obj) => {
  if (obj === null || typeof obj !== "object") return obj;

接着,根据传入对象的类型创建一个克隆对象,如果传入的是数组,则创建一个空数组,否则创建一个空对象。

let clone = Array.isArray(obj) ? [] : {};

然后,遍历原始对象的所有属性,如果该属性是原始对象自身的属性(不是原型链上的属性),则递归地对该属性进行深拷贝,并将其赋值给克隆对象的对应属性。如果该属性是日期对象,则对其进行特殊处理,创建一个新的日期对象,并将其时间戳设置为原始日期对象的时间戳。

for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] =
        obj[key] instanceof Date
          ? new Date(obj[key].getTime())
          : deepClone(obj[key]);
    }
  }

最后,返回克隆对象。

return clone;
};

最后做下验证,使用 deepClone 函数对 original 对象进行深度拷贝,得到了一个新的克隆对象 clone。在 clone 对象上进行修改并不会影响原始对象 original。

const clone = deepClone(original);

console.log(clone);
// Output: {a: 1, b: {c: 2022-12-31T08:00:00.000Z}}

clone.b.c.setFullYear(2023);

console.log(original);
// Output: {a: 1, b: {c: 2022-12-31T08:00:00.000Z}}

运行结果表明,尽管对 clone.b.c 进行了修改,但是原始对象 original 并没有受到影响,它的属性 b.c 仍然是一个日期对象,年份为 2022 年。

2、原生 structuredClone() 方法

structuredClone() 是 JavaScript 的原生方法,它是浏览器内置的 API 之一,可用于深拷贝对象。structuredClone() 方法在复制对象时会处理复杂的数据类型,并确保它们的引用关系得到正确处理。它是深拷贝对象的最佳选择之一,因为它可以同时处理各种数据类型,并保持它们的引用关系不变。此外,它还支持多种环境,包括 Web Workers 和 Service Workers 等。

使用structuredClone()进行深拷贝的优点是它是 JavaScript 的原生方法,内置于浏览器中,因此不需要额外的依赖。此外,它能够深度复制包括 Date,RegExp,Map 和 Set 等所有 JavaScript 基本类型和复杂类型数据,能够准确地复制各种对象和数据结构。在需要精确拷贝数据的场景中,如跨文档通信、存储和恢复应用程序状态等场景,使用该方法可以确保复制的对象完全相同,不会出现副作用。

然而,使用structuredClone()进行深拷贝的缺点是它只能用于浏览器环境,不能用于 Node.js 环境。此外,它不能复制一些特定的对象,如 Blob,File,ImageData,AudioContext 等。还有一点需要注意的是,由于该方法是基于序列化和反序列化实现的,所以它可能不适用于大型对象和数据结构,因为它可能会导致性能问题。

因此,在使用structuredClone()方法进行深拷贝时,需要根据具体情况权衡其优缺点,并确保使用它的场景是合适的。

延伸阅读

对于Blob,File,ImageData和AudioContext对象,这些对象本身是不支持序列化的,所以无法通过structuredClone()来深拷贝。因此,在这种情况下,你需要使用其他方法来拷贝这些对象。

对于Blob和File对象,你可以使用FileReader API或Blob API来复制这些对象。对于ImageData对象,你可以使用Canvas API来复制它。对于AudioContext对象,你可以使用Web Audio API中的其他方法来复制它。

需要注意的是,这些方法可能会比使用structuredClone()更加复杂,但是它们可以处理structuredClone()不能处理的对象类型。

举一个 structuredClone() 函数的示例:

当使用 postMessage() 在两个不同的窗口间传递数据时,需要对传递的数据进行序列化和反序列化操作。由于 postMessage() 仅支持结构化克隆算法,因此在传递复杂数据时,可以使用 structuredClone() 方法进行深拷贝,示例如下:

// 发送端
const data = { name: 'John', age: 30, hobbies: ['reading', 'swimming'] };
const transfer = [data.hobbies];
const clonedData = structuredClone(data); // 使用structuredClone()方法进行深拷贝
window.opener.postMessage(clonedData, '*', transfer);

// 接收端
window.addEventListener('message', function (e) {
  const data = e.data;
  const hobbies = data.hobbies; // 传递过来的数组对象
  console.log(data.name); // John
  console.log(data.age); // 30
  console.log(hobbies); // ['reading', 'swimming']
});

在上述示例中,我们将 data 对象通过 postMessage() 方法发送到父窗口,而 hobbies 数组则通过 transfer 数组传递,以便在传递数据时避免进行额外的复制操作。在接收端,我们使用 structuredClone() 方法对传递过来的 data 对象进行深拷贝,以保证在接收到数据后能够正确地读取其属性值。

以下是目前浏览器的兼容情况:

f37c08e66122da5547cc9220dbc9094a.jpeg

手写方法和原生方法的比较

对于大多数JavaScript对象和数据结构来说,structuredClone()是更好的深拷贝方法。因为它能够正确处理复杂的数据结构,包括日期、正则表达式等,而且可以支持跨文档、跨窗口或者跨 Worker 进行数据传输。另一方面,手写自定义递归实现深拷贝方法可以满足特定的需求,比如需要处理特定的数据类型或者需要更高的性能。但是需要注意的是,这种方法也可能会有一些缺陷和局限性,比如对于循环引用的处理可能不太好会有性能问题。因此,在选择深拷贝方法时需要根据具体情况进行选择。

结束

本文章主要介绍了在 JavaScript 中正确使用深拷贝的方法,想要在 JavaScript 对正确使用深拷贝的我们来说是非常有用的。深拷贝虽然在某些情况下会带来性能问题,但是在处理复杂数据结构时,它是非常有必要的。我们需要仔细评估自己的应用场景,选择适合自己的深拷贝方法。在实践中,我们需要时刻注意内存使用和性能问题,并根据具体情况选择最合适的深拷贝方法。深拷贝虽然能够处理复杂数据结构,但在某些情况下可能会影响性能,因此需要谨慎使用。最好的实践是在需要深拷贝时,先评估数据结构的复杂性和大小,建议使用我推荐的结构化克隆的方法。

今天的分享就到这里,希望对你有所帮助,感谢你的阅读,文章创作不易,如果你喜欢我的分享,别忘了点赞转发,让更多的人看到,最后别忘记关注「前端达人」,你的支持将是我分享最大的动力,后续我会持续输出更多内容,敬请期待。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JavaScript中,深拷贝和浅拷贝是关于对象和数组复制的概念。 浅拷贝是指创建一个新的对象或数组,新对象的属性值或数组元素是原始对象的引用。也就是说,新对象和原始对象共享相同的内存地址,当修改其中一个对象时,另一个对象也会受到影响。 深拷贝是指创建一个新的对象或数组,新对象的属性值或数组元素是原始对象的完全复制。也就是说,新对象和原始对象是完全独立的,修改其中一个对象不会影响另一个对象。 实现深拷贝方法有多种,下面介绍两种常见的方法: 1. 使用JSON.stringify()和JSON.parse():这种方法适用于没有包含函数、日期等特殊类型的简单对象和数组。首先,使用JSON.stringify()将原始对象转换为字符串,然后使用JSON.parse()将字符串转换回新的对象。 ```javascript var newObj = JSON.parse(JSON.stringify(oldObj)); ``` 这种方法的缺点是无法复制函数、日期等特殊类型,并且对于循环引用的对象也无法正确处理。 2. 递归复制:这种方法适用于复杂对象和数组,包括函数、日期等特殊类型。通过递归遍历原始对象的属性或数组元素,并创建新的对象或数组来实现深拷贝。 ```javascript function deepCopy(obj) { if (typeof obj !== 'object' || obj === null) { return obj; } var newObj = Array.isArray(obj) ? [] : {}; for (var key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = deepCopy(obj[key]); } } return newObj; } var newObj = deepCopy(oldObj); ``` 这种方法可以正确处理复杂对象和数组,但需要注意处理循环引用的情况,否则可能导致无限递归。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值