深拷贝是前端非常常用的功能,本文将会介绍几种实现方式和相关的库。
实现方式
先序列化后解析
JSON.parse(JSON.stringify())
这可能是最简单的方法,首先使用 JSON.stringify
将对象转成 JSON 字符串,然后再使用 JSON.parse
将字符串解析成新的对象。
但是它有以下这些限制:
- 如果对象里面有
undefined
、Date
、RegExp
等类型,序列化后的结果会丢失原有的类型信息,变成string
类型。 NaN
、Infinity
、-Infinity
序列化后会变成null
- 如果对象中出现循环引用,会导致代码陷入死循环,从而抛出错误。
因此,它只适用于一些简单的 JavaScript 对象,如果需要深拷贝复杂的对象,最好使用其他方法。
递归
我们可以使用递归来实现深拷贝。对于一个对象,我们可以递归地遍历它的每一个属性,然后将它们复制到一个新的对象中。如果属性的值也是一个对象,我们可以递归地处理它,直到所有属性都被复制到了新的对象。
代码如下:
function cloneDeep(obj) {
if (obj === null) return null
if (typeof obj !== 'object') return obj
const target = Array.isArray(obj) ? [] : {}
for (let key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
target[key] = cloneDeep(obj[key])
}
}
return target
}
这样一个简单的深克隆函数就完成了,但是它现在存在一个问题:如果对象中存在循环引用的话,就会报错:Uncaught RangeError: Maximum call stack size exceeded
function cloneDeep(obj, stack) {
if (obj === null) return null
if (typeof obj !== 'object') return obj
stack = stack || new WeakMap()
if (stack.has(obj)) return stack.get(obj)
const target = Array.isArray(obj) ? [] : {}
stack.set(obj, target)
for (let key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
target[key] = cloneDeep(obj[key], stack)
}
}
return target
}
开源库
Lodash
lodash 是 JavaScript 中一个非常流行的实用工具库,如果你还没有使用过它,建议自行了解。它其中包括了一个叫做 cloneDeep
的方法,可以深度复制对象,而且它的性能也很高1。使用这个方法,我们可以轻松地实现深拷贝,而且能够处理复杂的对象,包括循环引用和嵌套对象。
klona
klona 是一个超小的高性能克隆函数,它可以安全地处理 Array
、Date
、Map
、Object
、RegExp
、Set
、TypedArray
等数据类型。
其他
还有许多其他处理深克隆的库,例如:deep-copy、clone、clone-deep、rfdc 等等。
Immer 是一个专门用来处理不可变数据的库,它提供了一些非常方便的 API,可以让我们轻松地创建和修改不可变数据,而且它的性能也很高。如果需要处理大量的不可变数据,Immer 是一个不错的选择。
原生 API
structuredClone()
是 HTML5 中提供的一个 API,可以深拷贝一个对象。它能够处理循环引用和不同类型的数据2,包括 Date
、RegExp
等类型。但是它有以下这些限制:
- 只能在主线程中使用,不能在 Web Worker 中使用。
- 不能序列化函数、
DOM
节点等类型,更多内容请参考: 结构化克隆算法。 - 在 Safari 中,它不能序列化
ArrayBuffer
。
因此,它只适用于一些简单的 JavaScript 对象,如果需要深拷贝复杂的对象,最好使用其他方法。