JavaScript 关于深拷贝问题

问题

我正在尝试完成操作回撤功能, 需要保存各个操作后的状态, 状态内有一个对象存在循环引用和对象引用的问题, 我尝试通过这个深拷贝函数将它保存下来:

const is = {
    Array: Array.isArray,
    Date: (val) => val instanceof Date,
    Set: (val) => Object.prototype.toString.call(val) === '[object Set]',
    Map: (val) => Object.prototype.toString.call(val) === '[object Map]',
    Object: (val) => Object.prototype.toString.call(val) === '[object Object]',
    Symbol: (val) => Object.prototype.toString.call(val) === '[object Symbol]',
    Function: (val) => Object.prototype.toString.call(val) === '[object Function]'
}

function deepCopy(value, weakMap = new WeakMap()) { // 新增box的world无法深拷贝?
    // Function
    if (is.Function(value)) {
        return value;
    }

    if (is.Date(value)) return new Date(value.valueOf())

    if (is.Symbol(value)) return Symbol(value.description)

    if (is.Set(value)) {
        const newSet = new Set()
        for (const item of value) newSet.add(deepCopy(item), weakMap)
        return newSet
    }

    if (is.Map(value)) {
        const newMap = new Map()
        for (const item of value) newMap.set(deepCopy(item[0], weakMap), deepCopy(item[1], weakMap))
        return newMap
    }

    if (weakMap.has(value)) return weakMap.get(value)

    if (!(is.Object(value))) return value

    const newObj = is.Array(value) ? [] : {}

    weakMap.set(value, newObj)

    for (const key in value) {
        weakMap.set(value, newObj);
        newObj[key] = deepCopy(value[key], weakMap);
        if (is.Object(value[key])) {
            const proto = Object.getPrototypeOf(value[key]);
            Object.setPrototypeOf(newObj[key], proto);
        }
    }

    const symbolKeys = Object.getOwnPropertySymbols(value)
    for (const sKey of symbolKeys) {

        newObj[sKey] = deepCopy(value[sKey], weakMap)
    }

    return newObj
}
遇到的现象和发生背景,请写出第一个错误信息

对象内有一个数组, 该数组应当反应页面内所有的box, 但现在用户增加box后状态无法保存(即该数组内元素增加后的状态经过深拷贝依然受到对象引用影响).
如下图box数组:

img

增加操作会导致所有由增加操作得到的状态发生同步改变.

但用户删除box, 即该数组内元素减少后的状态可以正常保存下来, 由减少操作得到的状态之间却互不影响.

在调用该深拷贝函数前, 取到的状态均是完全正确.

我想知道这是否是深拷贝函数内的问题?
谢谢

你的深拷贝代码中存在一些问题。

首先,在使用 WeakMap 时,你在第一次遇到一个对象时将其设置为了 WeakMap 的键值,但在遍历对象中的属性时再次使用了 weakMap.set 方法,这样就又新建了一个键值。

其次,你在遍历对象属性时,在 newObj[key] = deepCopy(value[key], weakMap) 后再使用了 Object.setPrototypeOf(newObj[key], proto) 方法,但这不会影响到 newObj[key] 的原型链,只会在 newObj[key] 上设置一个不同的原型对象。

修改的方法如下:

function deepCopy(value, weakMap = new WeakMap()) {
if (is.Function(value)) return value;
if (is.Date(value)) return new Date(value.valueOf())
if (is.Symbol(value)) return Symbol(value.description)
if (is.Set(value)) {
const newSet = new Set()
for (const item of value) newSet.add(deepCopy(item), weakMap)
return newSet
}
if (is.Map(value)) {
const newMap = new Map()
for (const item of value) newMap.set(deepCopy(item[0], weakMap), deepCopy(item[1],weakMap))
return newMap
}
if (weakMap.has(value)) return weakMap.get(value)
if (!(is.Object(value))) return value
const newObj = is.Array(value) ? [] : {}
weakMap.set(value, newObj)
for (const key in value) {
newObj[key] = deepCopy(value[key], weakMap);
}
const symbolKeys = Object.getOwnPropertySymbols(value)
for (const sKey of symbolKeys) {
newObj[sKey] = deepCopy(value[sKey], weakMap)

}
return newObj
}

这样深拷贝函数就可以递归拷贝对象中的对象,也就能解决循环引用和对象引用的问题了。第二个问题就是, 数组中新增元素会被修改, 是因为在数组中使用的引用是指向同一地址的, 对数组的深拷贝需要特殊处理.
一种方法是使用循环,将数组中的每个元素单独深拷贝,将深拷贝后的元素放入新的数组中.
另一种方法是使用 JSON.parse(JSON.stringify(arr)) 转换成字符串再转换成对象的方式进行深拷贝,这样就可以完成对数组的深拷贝。

如果你需要更简单的做法可以使用工具库如 lodash 的 _.cloneDeep(arr) 方法,或者 使用 spread operator [...arr]

const newArr = [...arr]

或者

const newArr = Array.from(arr)

这些都可以完成数组的深拷贝.