带有循环引用的Javascript深度克隆对象

时间:2022-08-23 00:23:52

I have copied the function below from an existing answer by Dmitriy Pichugin. This function can deep clone an object without any circular references- it works.

我从Dmitriy Pichugin的现有答案中复制了下面的功能。此函数可以深度克隆对象而无需任何循环引用 - 它可以工作。

function deepClone( obj ) {
    if( !obj || true == obj ) //this also handles boolean as true and false
        return obj;
    var objType = typeof( obj );
    if( "number" == objType || "string" == objType ) // add your immutables here
        return obj;
    var result = Array.isArray( obj ) ? [] : !obj.constructor ? {} : new obj.constructor();
    if( obj instanceof Map )
        for( var key of obj.keys() )
            result.set( key, deepClone( obj.get( key ) ) );
    for( var key in obj )
    if( obj.hasOwnProperty( key ) )
            result[key] = deepClone( obj[ key ] );
    return result;
}

However, my program loops indefinitely and I have realised that this is due to a circular reference.

但是,我的程序无限循环,我意识到这是由于循环引用。

An example of a circular reference:

循环引用的示例:

function A() {}
function B() {}

var a = new A();
var b = new B();

a.b = b;
b.a = a;

3 个解决方案

#1


6  

I would suggest to use a Map to map objects in the source with their copy in the destination. In fact, I ended up using WeakMap as suggested by Bergi. Whenever a source object is in the map, its corresponding copy is returned instead of recursing further.

我建议使用Map来映射源中的对象及其在目标中的副本。事实上,我最终使用了Bergi建议的WeakMap。只要源对象在映射中,就会返回其相应的副本,而不是进一步递归。

At the same time some of the code in the original deepClone code can be optimised further:

同时,原始deepClone代码中的一些代码可以进一步优化:

  • The first part testing for primitive values has a small issue: it treats new Number(1) differently from new Number(2). This is because of the == in the first if. It should be changed to ===. But really, the first few lines of code seem then equivalent to this test: Object(obj) !== obj

    对原始值进行测试的第一部分有一个小问题:它将新数字(1)与新数字(2)区别对待。这是因为第一个if中的==。它应该改为===。但实际上,前几行代码似乎等同于这个测试:Object(obj)!== obj

  • I also rewrote some for loops into more functional expressions

    我还将一些for循环重写为更多功能表达式

This needs ES6 support:

这需要ES6支持:

function deepClone(obj, hash = new WeakMap()) {
    // Do not try to clone primitives or functions
    if (Object(obj) !== obj || obj instanceof Function) return obj;
    if (hash.has(obj)) return hash.get(obj); // Cyclic reference
    try { // Try to run constructor (without arguments, as we don't know them)
        var result = new obj.constructor();
    } catch(e) { // Constructor failed, create object without running the constructor
        result = Object.create(Object.getPrototypeOf(obj));
    }
    // Optional: support for some standard constructors (extend as desired)
    if (obj instanceof Map)
        Array.from(obj, ([key, val]) => result.set(deepClone(key, hash), 
                                                   deepClone(val, hash)) );
    else if (obj instanceof Set)
        Array.from(obj, (key) => result.add(deepClone(key, hash)) );
    // Register in hash    
    hash.set(obj, result);
    // Clone and assign enumerable own properties recursively
    return Object.assign(result, ...Object.keys(obj).map (
        key => ({ [key]: deepClone(obj[key], hash) }) ));
}
// Sample data
function A() {}
function B() {}
var a = new A();
var b = new B();
a.b = b;
b.a = a;
// Test it
var c = deepClone(a);
console.log('a' in c.b.a.b); // true

#2


1  

Since object cloning has a lot of pitfalls (circular references, proto chains, Set/Map, etc)
I suggest you to use one of well-tested popular solutions.

由于对象克隆存在很多陷阱(循环引用,原型链,Set / Map等),我建议您使用经过充分测试的流行解决方案之一。

Like, lodash's _.cloneDeep or 'clone' npm module.

比如,lodash的_.cloneDeep或'clone'npm模块。

#3


0  

You can store the references and results in separate arrays and when you find a property with the same reference you just need to return the cached result.

您可以将引用和结果存储在单独的数组中,当您找到具有相同引用的属性时,您只需返回缓存的结果。

function deepClone(o) {
    var references = [];
    var cachedResults = [];

    function clone(obj) {
        if (typeof obj !== 'object')
            return obj;
        var index = references.indexOf(obj);
        if (index !== -1)
            return cachedResults[index];
        references.push(obj);
        var result = Array.isArray(obj) ? [] :
            obj.constructor ? new obj.constructor() : {};
        cachedResults.push(result);
        for (var key in obj)
            if (obj.hasOwnProperty(key))
                result[key] = clone(obj[key]);
        return result;
    }
    return clone(o);
}

I removed the map and some of the other type comparisons to make it more readable.

我删除了地图和一些其他类型的比较,使其更具可读性。

Check @trincot's solid ES6 answer if you can target modern browsers.

如果您可以定位现代浏览器,请查看@ trincot的可靠ES6答案。

#1


6  

I would suggest to use a Map to map objects in the source with their copy in the destination. In fact, I ended up using WeakMap as suggested by Bergi. Whenever a source object is in the map, its corresponding copy is returned instead of recursing further.

我建议使用Map来映射源中的对象及其在目标中的副本。事实上,我最终使用了Bergi建议的WeakMap。只要源对象在映射中,就会返回其相应的副本,而不是进一步递归。

At the same time some of the code in the original deepClone code can be optimised further:

同时,原始deepClone代码中的一些代码可以进一步优化:

  • The first part testing for primitive values has a small issue: it treats new Number(1) differently from new Number(2). This is because of the == in the first if. It should be changed to ===. But really, the first few lines of code seem then equivalent to this test: Object(obj) !== obj

    对原始值进行测试的第一部分有一个小问题:它将新数字(1)与新数字(2)区别对待。这是因为第一个if中的==。它应该改为===。但实际上,前几行代码似乎等同于这个测试:Object(obj)!== obj

  • I also rewrote some for loops into more functional expressions

    我还将一些for循环重写为更多功能表达式

This needs ES6 support:

这需要ES6支持:

function deepClone(obj, hash = new WeakMap()) {
    // Do not try to clone primitives or functions
    if (Object(obj) !== obj || obj instanceof Function) return obj;
    if (hash.has(obj)) return hash.get(obj); // Cyclic reference
    try { // Try to run constructor (without arguments, as we don't know them)
        var result = new obj.constructor();
    } catch(e) { // Constructor failed, create object without running the constructor
        result = Object.create(Object.getPrototypeOf(obj));
    }
    // Optional: support for some standard constructors (extend as desired)
    if (obj instanceof Map)
        Array.from(obj, ([key, val]) => result.set(deepClone(key, hash), 
                                                   deepClone(val, hash)) );
    else if (obj instanceof Set)
        Array.from(obj, (key) => result.add(deepClone(key, hash)) );
    // Register in hash    
    hash.set(obj, result);
    // Clone and assign enumerable own properties recursively
    return Object.assign(result, ...Object.keys(obj).map (
        key => ({ [key]: deepClone(obj[key], hash) }) ));
}
// Sample data
function A() {}
function B() {}
var a = new A();
var b = new B();
a.b = b;
b.a = a;
// Test it
var c = deepClone(a);
console.log('a' in c.b.a.b); // true

#2


1  

Since object cloning has a lot of pitfalls (circular references, proto chains, Set/Map, etc)
I suggest you to use one of well-tested popular solutions.

由于对象克隆存在很多陷阱(循环引用,原型链,Set / Map等),我建议您使用经过充分测试的流行解决方案之一。

Like, lodash's _.cloneDeep or 'clone' npm module.

比如,lodash的_.cloneDeep或'clone'npm模块。

#3


0  

You can store the references and results in separate arrays and when you find a property with the same reference you just need to return the cached result.

您可以将引用和结果存储在单独的数组中,当您找到具有相同引用的属性时,您只需返回缓存的结果。

function deepClone(o) {
    var references = [];
    var cachedResults = [];

    function clone(obj) {
        if (typeof obj !== 'object')
            return obj;
        var index = references.indexOf(obj);
        if (index !== -1)
            return cachedResults[index];
        references.push(obj);
        var result = Array.isArray(obj) ? [] :
            obj.constructor ? new obj.constructor() : {};
        cachedResults.push(result);
        for (var key in obj)
            if (obj.hasOwnProperty(key))
                result[key] = clone(obj[key]);
        return result;
    }
    return clone(o);
}

I removed the map and some of the other type comparisons to make it more readable.

我删除了地图和一些其他类型的比较,使其更具可读性。

Check @trincot's solid ES6 answer if you can target modern browsers.

如果您可以定位现代浏览器,请查看@ trincot的可靠ES6答案。