В Javascript при выполнении глубокой копии, как мне избежать цикла, из-за свойства "this"?

У меня есть некоторый код библиотеки, который бесконечно циклически меняет.

Я не понимаю, как лучше всего выполнять обнаружение циклов и избегать в 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
                }

Ответ 1

"Надежный и чистый" способ, которым я знаю, иметь дело с этой ситуацией, - это использовать коллекцию "посещенных" объектов, а затем реагировать - завершать, вставлять символическую ссылку и т.д. - на основе того, если текущий объект уже был "посещен" или нет.

г. Крокфорд использует этот подход в 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. В то время как границы коллекции массива O(n^2), это не так плохо, как кажется:

Это связано с тем, что, если "посещенная" коллекция является только защитой, то значение n - это только глубина стека: только циклы имеют значение при одновременном копировании одного и того же объекта. То есть, объекты в "посещенной" коллекции можно обрезать с помощью stack-unwind.

В коде cycle.js "посещенная" коллекция не может быть обрезана, поскольку она должна гарантировать, что всегда используется одинаковое символическое имя для данного объекта, что позволяет сериализации "поддерживать ссылки" при ее восстановлении. Однако даже в этом случае n - это количество пройденных уникальных не-примитивных значений.

Единственный другой метод, о котором я могу думать, потребует добавления "посещенного свойства" непосредственно к пройденным объектам, что я бы рассмотрел, как правило, нежелательную функцию. (Однако, см. Комментарий Берги об этом артефакте [относительно] легко очищается.)

Счастливое кодирование.

Ответ 2

ОК, меня заинтересовало то, как это "посещаемое" свойство @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.
  • дескрипторы свойств, такие как сеттеры/геттеры, ненарушаемые и невыносимые свойства.

Плюсы:

  • он не использует большой массив, который нужно искать каждый раз, когда он сталкивается с объектом.

Недостатки:

  • не работает с объектами, у которых есть метод __getDeepCircularCopy__, который не соответствует требованиям. Хотя методы (свойства с функцией как значение) в любом случае не поддерживаются в этой облегченной версии.

Это решение будет работать на объектах с круговыми ссылками, копируя круговую структуру, не заканчивая бесконечным циклом. Обратите внимание, что "круговой" означает здесь, что свойство ссылается на одного из его "родителей" в "дереве":

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

Структура деревьев, разделяющих лист, не будет скопирована, они станут двумя независимыми листьями:

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

Ответ 3

Если вы не хотите отслеживать все скопированные свойства.

Но если вы уверены, что каждое свойство имеет значение null, строку, число, массив или простой объект, вы можете поймать исключения JSON.stringify, чтобы увидеть, есть ли ссылки на обратную связь, например:

try {
    JSON.stringify(obj);
    // It 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.
}

Это просто идея, и я не имею представления о выступлениях. Я боюсь, что на больших объектах может быть довольно медленно.

Ответ 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!"
// }