在 Javascript 中,当执行深度复制时,由于属性是“this”,我如何避免循环?

Posted

技术标签:

【中文标题】在 Javascript 中,当执行深度复制时,由于属性是“this”,我如何避免循环?【英文标题】:In Javascript, when performing a deep copy, how do I avoid a cycle, due to a property being "this"? 【发布时间】:2012-05-30 12:01:49 【问题描述】:

我有一些库代码在我身上无休止地循环。

我不清楚如何最好地在 javascript 中执行循环检测和避免。 即没有检查对象是否来自“this”引用的编程方式,是吗?

这是代码。 谢谢!

setAttrs: function(config) 
    var go = Kinetic.GlobalObject;
    var that = this;

    // set properties from config
    if(config !== undefined) 
        function setAttrs(obj, c) 
            for(var key in c) 
                var val = c[key];

                /*
                 * if property is an object, then add an empty object
                 * to the node and then traverse
                 */
                if(go._isObject(val) && !go._isArray(val) && !go._isElement(val)) 
                    if(obj[key] === undefined) 
                        obj[key] = ;
                    
                    setAttrs(obj[key], val);  // <--- offending code; 
                                              // one of my "val"s is a "this" reference
                                              // to an enclosing object
                

【问题讨论】:

跟踪“已访问”对象的集合,不要重新访问该集合中的对象。数组可以工作,但没有良好的界限......并且任意对象不会成为对象中的好键。 我想到了这个……但似乎没有更好的词,重量级?即没有更好的解决方案? 我认为在遍历对象图时这是一种非常常见的方法。当然,在这种情况下它更尴尬,因为 JavaScript 中没有身份映射。另一个(讨厌的)替代方法是使用“访问的属性”修改遍历的对象。 这就是克罗克福德先生在cycle.js中的做法@ 【参考方案1】:

我所知道的处理这种情况的“可靠和干净”的方法是使用“已访问”对象的集合,然后根据当前对象是否已经做出反应——终止、插入符号引用等是否被“访问”过。

先生。 Crockford 在cycle.js 中使用了这种方法,他使用一个数组来收集。摘录:

// If the value is an object or array, look to see if we have already
// encountered it. If so, return a $ref/path object. This is a hard way,
// linear search that will get slower as the number of unique objects grows.

for (i = 0; i < objects.length; i += 1) 
    if (objects[i] === value) 
        return $ref: paths[i];
    

不幸的是,无法在 JavaScript 中为此使用原始的“哈希”方法,因为它缺少 Identity-Map。虽然 Array-collection 的边界是 O(n^2),但 并没有听起来那么糟糕

这是因为,如果“已访问”集合只是一个守卫,那么 n 的值就是堆栈的深度:只有循环很重要,而多次复制同一个对象则不重要。也就是说,“已访问”集合中的对象可以在堆栈展开时进行修剪。

在 cycle.js 代码中,“已访问”集合不能被修剪,因为它必须确保始终使用给定对象的相同符号名称,这允许序列化在恢复时“保持引用”。然而,即使在这种情况下,n 只是遍历的唯一非原始值的数量。

我能想到的唯一其他方法需要直接向正在遍历的对象添加“已访问属性”,我认为这是一个通常不受欢迎的功能。 (但是,请参阅 Bergi 的评论,关于这个神器[相对] 很容易清理。)

编码愉快。

【讨论】:

我不认为“已访问”属性是个坏主意 - 如果您之后将其删除... @Bergi 够公平的。不过,我仍然觉得在“复制”操作中修改对象很脏。 是的,我也是。此外,具有递归深度优先算法的“已访问”属性方法不适用于任何交叉引用结构,它只检测圆圈。看我的回答:) 很好的答案...我对“只有循环很重要,而多次复制同一个对象不重要”有点困惑。每个原始对象只有一个克隆对象不是最好的吗?即,如果原始对象中有多个对单个对象的引用,则所有这些引用仍将指向结果中的单个对象。这可能是用户所期望的,并且使用更少的内存,不是吗?【参考方案2】:

除非您想跟踪复制的每个属性,否则不会。

但是如果你确定每个属性要么是null,要么是字符串,要么是数字,要么是数组,要么是一个简单的对象,你可以捕获JSON.stringify异常来查看是否有反向引用,像这样:

try 
    JSON.stringify(obj);
    // It's ok to make a deep copy of obj
 catch (e) 
    // obj has back references and a deep copy would generate an infinite loop
    // Or finite, i.e. until the stack space is full.

这只是一个想法,我对表演一无所知。我担心它在大型物体上可能会很慢。

【讨论】:

【参考方案3】:

好的,我对@pst 提到的“已访问”属性的外观很感兴趣,所以我编写了以下代码:

Object.copyCircular = function deepCircularCopy(o) 
    const gdcc = "__getDeepCircularCopy__";
    if (o !== Object(o))
        return o; // primitive value
    var set = gdcc in o,
        cache = o[gdcc],
        result;
    if (set && typeof cache == "function")
        return cache();
    // else
    o[gdcc] = function()  return result; ; // overwrite
    if (o instanceof Array) 
        result = [];
        for (var i=0; i<o.length; i++) 
            result[i] = deepCircularCopy(o[i]);
     else 
        result = ;
        for (var prop in o)
            if (prop != gdcc)
                result[prop] = deepCircularCopy(o[prop]);
            else if (set)
                result[prop] = deepCircularCopy(cache);
    
    if (set)
        o[gdcc] = cache; // reset
    else
        delete o[gdcc]; // unset again
    return result;
;

注意,这只是一个例子。不支持:

非普通对象。所有带有原型的东西(数组除外)都不会被克隆,而是复制到new Object!这包括函数! 跨全局范围的对象:它使用instanceof Array。 属性描述符,例如 setter/getter、不可写和不可枚举的属性

好东西:

它不使用每次遇到对象时都需要搜索的大数组。

缺点:

不适用于具有__getDeepCircularCopy__ 方法的对象,该方法不符合它的要求。尽管在这个轻量级版本中无论如何都不支持方法(以函数为值的属性)。

此解决方案适用于具有循环引用的对象,复制循环结构,不会以无限循环结束。请注意,“循环”在这里表示属性引用“树”中的其中一个“父级”:

   [Object]_            [Object]_
     /    |\              /    |\
   prop     |           prop    |
     \_____/             |      |
                        \|/     |
                      [Object]  |
                          \     |
                         prop   |
                            \___/

共享一个叶子的树的结构不会被复制,它们会成为两个独立的叶子:

            [Object]                     [Object]
             /    \                       /    \
            /      \                     /      \
          |/_      _\|                 |/_      _\|  
      [Object]    [Object]   ===>  [Object]    [Object]
           \        /                 |           |
            \      /                  |           |
            _\|  |/_                 \|/         \|/
            [Object]               [Object]    [Object]

【讨论】:

漂亮的图表和解释。但是,我认为如果属性包含“符号引用名称”(例如在 cycle.js 中使用),也可以连接到查找以维护关系(文本序列化需要符号查找)。或者,存储的属性(如果存在)可以评估为 新 [第一个] 克隆的 对象,并在检测时返回它...【参考方案4】:

这是一个简单的递归克隆方法。与许多其他解决方案一样,大多数非基本属性将与原始对象(例如函数)共享一个引用。

它通过保存引用对象的映射来处理无限循环,以便后续引用可以共享同一个克隆。

const georgeCloney = (originalObject, __references__ = new Map()) => 
  if(typeof originalObject !== "object" || originalObject === null) 
    return originalObject;
  

  // If an object has already been cloned then return a
  // reference to that clone to avoid an infinite loop
  if(__references__.has(originalObject) === true) 
    return __references__.get(originalObject);
  

  let clonedObject = originalObject instanceof Array ? [] : ;

  __references__.set(originalObject, clonedObject);

  for(let key in originalObject) 
    if(originalObject.hasOwnProperty(key) === false) 
      continue;
    

    clonedObject[key] = georgeCloney(originalObject[key], __references__);
  

  return clonedObject;
;

示例用法...

let foo = ;
foo.foo = foo;

let bar = georgeCloney(foo);
bar.bar = "Jello World!";

// foo output
// 
//   foo: 
//     foo: ...
//   
// 
// 
// bar output
//  
//   foo:  
//     foo: ...,
//     bar: "Jello World!"
//   , 
//   bar: "Jello World!"
// 

【讨论】:

【参考方案5】:

我必须这样做才能面试,这就是我得到的:

Object.defineProperty(Object.prototype, 'deepclone',  enumerable :false, configurable :true, value :function()
  let map /*for replacement*/ =new Map(), rep = ;map.set(this, rep)
  let output = (function medclone(_, map)  let output =..._
    for(let k in _)  let v =_[k]
      let v2 ;if(!(v instanceof Object)) v2 =v ;else 
        if(map.has(v)) v2 =map.get(v) ;else 
          let rep =
          map.set(v, rep), v2 =medclone(v, map)
          Replace(rep, v2), map.set(v, v2)
        
      
      output[k] =v2
        return output
  )(this, map)
  Replace(rep, output)
  return output
  /*[*/ function Replace(rep/*resentative*/, proper, branch =proper, seens =new Set())
    for(let k in branch)  let v =branch[k]
      if(v ===rep) branch[k] =proper
      else if(v instanceof Object &&!seens.has(v)) seens.add(v), Replace(rep, proper, v, seens)
    
  /*]*/
 )
                                                                                                                                  // Code is freely available for use for academia/scrutiny. As for development, contact author. 

方法是“砍杀”而不是“在编码之前有充足的时间计划”,因此结果不可能那么好,但它确实有效。可以对其进行迭代以删除递归入口点中的 WET。

【讨论】:

以上是关于在 Javascript 中,当执行深度复制时,由于属性是“this”,我如何避免循环?的主要内容,如果未能解决你的问题,请参考以下文章

如何在不替换 ES6/Javascript 中的整个属性的情况下深度复制对象 [重复]

检查 JavaScript 中是不是存在深度嵌套对象属性的最简单方法是啥? [复制]

深入探讨JavaScript如何实现深度复制(deep clone)

当用户单击使用 JavaScript 的按钮时,如何关闭当前浏览器选项卡? [复制]

JavaScript 中的对象深度复制(Object Deep Clone)

javascript 数组的深度复制