JS 现代化的深克隆

前端手写深拷贝/深克隆是一道回头率超高的笔试题,但笔试版一般不适用于生产环境,JSON 的奇技淫巧和 Lodash 的工具函数也各有缺点。

您知道吗,JS 现在有一种原生方法可以深层复制对象?

structuredClone 函数内置在 JS 运行时中:

const calendarEvent = {
  title: '攻城狮',
  date: new Date(111),
  attendees: ['Steve']
}

const copied = structuredClone(calendarEvent)

您是否注意到,上述示例中我们不仅复制了对象,还复制了嵌套数组,甚至是 Date 对象?

一切都如期工作:

copied.attendees // ["Steve"]
copied.date // Date: Wed Dec 31 1969 16:00:00
cocalendarEvent.attendees === copied.attendees // false

structuredClone 不仅可以如上操作,还可以:

  • 克隆无限嵌套的对象和数组

  • 克隆循环引用

  • 克隆各种 JavaScript 类型,比如 Date 、 Set 、 Map 、 Error 、 RegExp 、 ArrayBuffer 、 Blob 、 File 、 ImageData 等等

  • 传送任何可转移对象(transferable objects)

举个栗子,这种奇葩操作甚至也会如期工作:

const kitchenSink = {
  set: new Set([1, 3, 3]),
  map: new Map([[1, 2]]),
  regex: /foo/,
  deep: { array: [new File(someBlobData, 'file.txt')] },
  error: new Error('Hello!')
}
kitchenSink.circular = kitchenSink

// ✅ 一切顺利,完整的深拷贝!
const clonedSink = structuredClone(kitchenSink)

1. 为什么不选择展示对象克隆呢?

注意,我们正在谈论的是深拷贝。如果您只需浅拷贝,即不复制嵌套对象或数组的副本,那么我们可以直接展开对象克隆:

const simpleEvent = {
  title: '攻城狮'
}
// ✅ 问题不大,此处没有嵌套对象/数组
const shallowCopy = { ...calendarEvent }

或者其他备胎,只要您愿意:

const shallowCopy = Object.assign({}, simpleEvent)
const shallowCopy = Object.create(simpleEvent)

虽然但是,一旦我们嵌套了元素,我们就会遭遇“滑铁卢”:

const calendarEvent = {
  title: '攻城狮',
  date: new Date(123),
  attendees: ['Steve']
}

const shallowCopy = { ...calendarEvent }

// 🚩 夭寿啦:我们同时在 calendarEvent 及其副本中添加了 Bob
shallowCopy.attendees.push('Bob')

// 🚩 天呢噜:我们同时为 calendarEvent 及其副本更新了 date
shallowCopy.date.setTime(456)

如你所见,我们没有完整拷贝该对象。

嵌套日期和数组仍然是两者之间的共享引用,如果我们想编辑那些被认为只会更新 calendarEvent 对象副本的内容,这可能会给我们带来无妄之灾。

2. 为什么不选择JSON.parse(JSON.stringify(i))呢?

它实际上是一个很棒的点子,且具有惊人的性能,但存在若干 structuredClone 解决了的短板。

如下所示:

const calendarEvent = {
  title: '攻城狮',
  date: new Date(123),
  attendees: ['Steve']
}

//  JSON.stringify 会把 date 转换为字符串
const problematicCopy = JSON.parse(JSON.stringify(calendarEvent))

如果我们打印 problematicCopy,我们会看到:

{
  title: "攻城狮",
  date: "1970-01-01T00:00:00.123Z"
  attendees: ["Steve"]
}

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

发生这种情况是因为 JSON.stringify 只能处理基本对象、数组和原始值。处理任何其他类型都十分佛系。举个栗子,Date 被转换为字符串。但 Set 则转换为 {}

JSON.stringify 甚至完全无视某些内容,比如 undefined 或函数。

举个栗子,如果我们使用此方法复制 kitchenSink

const kitchenSink = {
  set: new Set([1, 3, 3]),
  map: new Map([[1, 2]]),
  regex: /foo/,
  deep: { array: [new File(someBlobData, 'file.txt')] },
  error: new Error('Hello!')
}

const veryProblematicCopy = JSON.parse(JSON.stringify(kitchenSink))

结果如下:

{
  "set": {},
  "map": {},
  "regex": {},
  "deep": {
    "array": [
      {}
    ]
  },
  "error": {},
}

我们必须删除最初为此使用的循环引用,因为如果 JSON.stringify 遭遇其中之一,就能且仅能报错。

因此,虽然如果我们的需求刚好符合其功能,这个方法自然棒棒哒,但我们可以用 structuredClone 肝一大坨事情(也就是上述我们未能做到的事情),而此方法却做不到。

3. 为什么不选择_.cloneDeep呢?

迄今为止,Lodash 的 cloneDeep 函数已经是解决此问题的一个十分常见的解决方案。

事实上,这确实能如期工作:

import cloneDeep from 'lodash/cloneDeep'

const calendarEvent = {
  title: '攻城狮',
  date: new Date(123),
  attendees: ['Steve']
}

// ✅ 一切顺利!
const clonedEvent = structuredClone(calendarEvent)

虽然但是,此时有且仅有一个警告。根据本人 IDE 中的导入成本(import cost)扩展,它会打印我导入的任何内容的 kb 成本,该函数压缩后总共有 17.4kb(gzip 压缩后为 5.3kb):

而这是假设您只导入了该函数的情况。如果您以更常见的方式导入,却没有意识到 Tree Shaking 优化并不总是如期奏效,您可能会一不小心仅针对这一函数导入多达 25kb 的数据 😱

虽然这对任何人而言都不会是世界末日,但在我们的例子中根本没有必要,尤其是浏览器已经内置了 structuredClone

4. structuredClone的短板

无法克隆函数

这会报错 —— DataCloneError 异常:

// 🚩 报错!
structuredClone({ fn: () => {} })

DOM节点

梅开二度 —— DataCloneError 异常:

// 🚩 报错!
structuredClone({ el: document.body })

5. 属性描述符,setters和getters

类似的类元数据(metadata-like)的功能也无法被克隆。

举个栗子,使用 getter 时,会克隆结果值,但不会克隆 getter 函数本身(或任何其他属性元数据):

structuredClone({
  get foo() {
    return 'bar'
  }
})
// 结果变成: { foo: 'bar' }

6. 对象原型

原型链不会被遍历或重复。因此,如果您克隆 MyClass 的实例,那么克隆对象将不再被视为此类的实例(但此类的所有有效属性都将被克隆)

class MyClass {
  foo = 'bar'
  myMethod() {
    /* ... */
  }
}
const myClass = new MyClass()

const cloned = structuredClone(myClass)
// 结果变成: { foo: 'bar' }

cloned instanceof myClass // false

7. 支持的类型的完整列表

简而言之,下述列表中未列出的任何内容都无法克隆:

JS内置函数

ArrayArrayBufferBooleanDataViewDateError 类型(那些下面具体列出),Map,仅限于普通对象的 Object(比如来自对象字面量),除了 symbol 的原始类型(又名 numberstringnullundefinedbooleanBigInt)、RegExpSetTypedArray

Error类型

  • Error

  • EvalError

  • RangeError

  • ReferenceError

  • SyntaxError

  • TypeError

  • URIError

Web/API类型

  • AudioData

  • Blob

  • CryptoKey

  • DOMException

  • DOMMatrix

  • DOMMatrixReadOnly

  • DOMPoint

  • DomQuad

  • DomRect

  • File

  • FileList

  • FileSystemDirectoryHandle

  • FileSystemFileHandle

  • FileSystemHandle

  • ImageBitmap

  • ImageData

  • RTCCertificate

  • VideoFrame

浏览器和运行时支持

这是最好的部分 —— 所有主流浏览器都支持 structuredClone,甚至包括 Node.js 和 Deno。

添加好友备注【进阶学习】拉你进技术交流群

  • 30
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在 JavaScript 中,克隆一个对象通常可以通过递归实现。具体来说,需要遍历对象的每一个属性,如果某个属性是对象或数组,那么需要递归地进行克隆,否则直接将其赋值给新对象即可。 以下是一个简单的克隆实现: ```javascript function deepClone(obj) { if (obj === null || typeof obj !== "object") { // 如果不是对象或数组,则直接返回 return obj; } const clone = Array.isArray(obj) ? [] : {}; // 判断是数组还是对象 // 遍历每个属性并递归克隆 for (let key in obj) { clone[key] = deepClone(obj[key]); } return clone; } ``` 这个函数可以接受任何类型的对象作为参数,并返回一个克隆后的对象。需要注意的是,该函数有一些限制: - 该函数不支持循环引用,如果原对象中有循环引用,将会导致栈溢出错误。 - 该函数不会克隆对象的原型链,只会克隆自身的属性。 因此,在使用这个函数时需要注意它的限制。如果需要处理循环引用或者原型链,需要使用更为复杂的实现。 ### 回答2: 在JavaScript中实现对象克隆,可以通过以下步骤实现: 1. 创建一个函数,命名为deepClone,该函数将接收一个参数obj,表示需要克隆对象。 2. 首先判断obj的类型,如果obj是基本数据类型或者是null,则直接返回该值,因为基本数据类型是按值传递的,不存在引用关系。 3. 创建一个变量cloneObj,用于保存克隆后的对象。 4. 遍历obj的属性,使用for...in循环遍历obj的所有可枚举属性。 5. 在遍历的过程中,判断obj的属性值的类型,如果属性值是对象或者数组,则递归调用deepClone函数进行克隆,将返回的克隆值赋给cloneObj的对应属性。 6. 如果属性值是基本数据类型或者是函数,则直接赋值给cloneObj的对应属性。 7. 最后,返回cloneObj,即为克隆后的对象。 以下是一个实现克隆的代码示例: function deepClone(obj) { if (obj === null || typeof obj !== "object") { return obj; } let cloneObj = Array.isArray(obj) ? [] : {}; for (let key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { cloneObj[key] = deepClone(obj[key]); } } return cloneObj; } 使用方法如下: const obj = { name: "Alice", age: 20, hobbies: ["coding", "reading"], address: { city: "Beijing", country: "China" } }; const clonedObj = deepClone(obj); console.log(clonedObj); 上述代码能够将obj克隆得到一个独立的对象clonedObj,两者完全独立,修改其中一个对象不会影响另一个对象。 ### 回答3: 对象克隆是指将一个对象完全复制一份,并且新对象与原对象没有任何引用关系。Javascript实现对象克隆可以通过以下步骤: 1. 创建一个空的目标对象,用于存储克隆后的对象。 2. 遍历原始对象的属性,使用递归的方式克隆每个属性的值。 3. 对于原始对象的每个属性,判断其类型: - 若为基本数据类型(如字符串、数字、布尔值等),直接复制到目标对象。 - 若为引用类型(如对象、数组等),则需要再次进行克隆操作。 4. 对于引用类型的属性,根据其具体类型进行不同的处理: - 若为数组,创建一个新的空数组,并遍历原数组,递归地将每个元素克隆到新数组中。 - 若为对象,创建一个空对象,并遍历原对象的属性,递归地将每个属性值克隆到新对象中。 5. 将克隆后的属性值赋值给目标对象对应的属性。 6. 最后返回目标对象,完成克隆操作。 下面是一个简单的Javascript代码示例: function cloneObject(source) { let target = {}; for(let key in source) { if(typeof source[key] === 'object' && source[key] !== null) { if(source[key].constructor === Array) { // 克隆数组 target[key] = source[key].map(item => cloneObject(item)); } else { // 克隆对象 target[key] = cloneObject(source[key]); } } else { // 复制基本数据类型 target[key] = source[key]; } } return target; } 使用该函数进行对象克隆: let obj1 = {a: 1, b: {c: 2}}; let obj2 = cloneObject(obj1); console.log(obj2); // 输出: {a: 1, b: {c: 2}} console.log(obj1 === obj2); // 输出: false
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值