【问题标题】:Javascript Deep Clone Object with Circular References带有循环引用的 Javascript 深度克隆对象
【发布时间】:2017-03-10 13:45:42
【问题描述】:

我从 Dmitriy Pichugin 的 existing answer 复制了以下函数。这个函数可以在没有任何循环引用的情况下深度克隆一个对象——它可以工作。

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;
}

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

循环引用示例:

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

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

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

【问题讨论】:

标签: javascript node.js clone circular-reference


【解决方案1】:

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

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);
}

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

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

【讨论】:

  • 啊,我得到了异常 Uncaught RangeError: Maximum call stack size exceeded(…),这似乎是由于在 deepClone 函数的第 19 行重复调用造成的。我在问题中的示例 ab 对象上使用了它。
  • @Lolums 我用更正检查更新了我的答案。请注意,该功能被精简了一点,以使其更易于理解:)
  • 不使用referencescachedResults,这是WeakMap 的完美用例!
  • @Bergi 那将是一个很棒的补充!
  • 谢谢 - 现在可以使用了。好吧,几乎:deepClone(a) 返回A { b: B {a: undefined} }
【解决方案2】:

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

同时原deepClone代码中的部分代码还可以进一步优化:

  • 原始值的第一部分测试有一个小问题:它对待new Number(1)new Number(2) 不同。这是因为第一个if 中的==。应该改为===。但实际上,前几行代码似乎相当于这个测试:Object(obj) !== obj

  • 我还将一些 for 循环改写为更多函数表达式

这需要 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

【讨论】:

  • 我相信如果我的对象构造函数之一期望接收参数,这将失败。 function A(foo) {if (!foo) throw Error('pass foo!')}
  • @Artin,deepClone 函数如何知道要传递给构造函数的参数?据我所知,JavaScript 对象不维护有关将哪些参数传递给构造函数的信息。
  • @Artin,不使用 Object.create(obj.constructor) 调用构造函数(正如您在已删除的评论中建议的那样)的替代方法可能会导致其他问题:运行构造函数对于对象位于一致的状态,它不会有任何自己的属性。
  • @trincon,你能举个例子吗?我认为它工作正常jsbin.com/bijaloziko/1/edit?js,console
  • 如果在构造函数中定义一个返回foo的方法呢?
【解决方案3】:

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

喜欢lodash's _.cloneDeep'clone' npm module

【讨论】:

  • 这是这个问题唯一合理的答案,没有必要重新发明轮子。
  • @KamilBęben 如果您自己实施解决方案很简单,那么您应该 10 次中的 10 次。当你使用一个库时,它会带来一堆你从未考虑过的考虑,并且以一种你可能不会的方式固执己见。
【解决方案4】:
const cloneDeep = src => {
  const clones = new WeakMap()
  return (function baseClone(src) {
    if (src !== Object(src)) {
      return src
    }
    if (clones.has(src)) {
      return clones.get(src)
    }
    const clone = {}
    clones.set(src, clone)
    Object.entries(src).forEach(([k, v]) => (clone[k] = baseClone(v)))
    return clone
  })(src)
}

const a = { x: 1 }
a.y = a
console.log(cloneDeep(a)) // <ref *1> { x: 1, y: [Circular *1] }

【讨论】:

    【解决方案5】:

    基本思想是要避免“超出最大调用堆栈”的情况。

    为了做到这一点,当需要克隆对象时,您需要首先创建一个空对象,将其缓存(使用WeakMap 之类的东西),然后更新该缓存对象的属性。如果您在创建副本后尝试对其进行缓存,则在再次递归引用时它不会被缓存(因此会出现调用堆栈错误)。

    你想做这样的事情:

    function deepClone(value, cache = new WeakMap()) {
      if(cache.has(value)) {
        return cache.get(value)
      }
      if(isPrimitive(value) || isFunction(value)) {
        return value;
      }
      if(value instanceof AnyParticularClass) {
        // handle specially in any cases you plan to support
      }
      if(isObject(value)) {
        const result = {}
        cache.set(value, result);
        Object.entries(value).forEach(function([key, val]) {
          result[key] = deepClone(val, cache);
        })
        return result
      }
      if(Array.isArray(value)) {
        return value.map(item => cloneDeep(item, cache));
      }
    }
    

    此外,克隆函数没有任何意义,因此只需将其返回即可。处理类的实例需要处理

    【讨论】:

      猜你喜欢
      • 2015-01-08
      • 2010-09-09
      • 2012-06-05
      • 2013-09-06
      • 2019-09-06
      • 2013-10-11
      • 2018-03-21
      相关资源
      最近更新 更多