Есть ли способ проверить циркулярную ссылку в JavaScript?

Я делаю игру, и я столкнулся с проблемой... Когда я пытаюсь сохранить, JSON терпит неудачу и сообщает, что где-то делается круговая ссылка. Я не думаю, что это на самом деле, я не вижу этого, так есть ли алгоритм или что-нибудь, что могло бы сказать мне, где именно (между которыми объекты и прочее)? Кроме того, есть ли альтернатива JSON, которая может сохранять циклическую ссылку? Я запускаю сервер node.js, я видел этот, но я не могу заставить его работать (это не сделано как модуль я может потребовать() в моем коде).

Ответ 1

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

Итак, вместо того, чтобы хранить круговую ссылку, вы просто сохраняете указатель на объект. Указатель будет просто похож на ref : '#path.to.object', который может быть разрешен при десериализации, поэтому вы указываете ссылку на фактический объект. Вам просто нужно разбить ссылку на сериализацию, чтобы иметь возможность сериализовать ее.

Обнаружение циклической ссылки в JavaScript может быть выполнено путем рекурсивного итерации по всем объектам (с помощью for (x in y)), сохраните x в массиве и сравните каждый x с оператором идентификации (aka строгий оператор сравнения) === для каждого z во временном массиве. Всякий раз, когда x === z равно true, замените ссылку на x на заполнитель, который будет сериализован для вышеупомянутого ref.

Альтернативой хранению массива над "посещенными" объектами является "испортить" объекты, которые вы повторяете, установив для них свойство, как в этом очень наивном примере:

for (x in y) {
    if (x.visited) {
       continue;
    }

    x.visited = true;
}

Ответ 2

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

function isCircularObject(node, parents){
    parents = parents || [];

    if(!node || typeof node != "object"){
        return false;
    }

    var keys = Object.keys(node), i, value;

    parents.push(node); // add self to current path      
    for(i = keys.length-1; i>=0; i--){
        value = node[keys[i]];
        if(value && typeof value == "object"){
            if(parents.indexOf(value)>=0){
                // circularity detected!
                return true;
            }
            // check child nodes
            if(arguments.callee(value, parents)){
                return true;
            }

        }
    }
    parents.pop(node);
    return false;
}

И использование будет isCircularObject(obj_value), где функция возвращает true, если существует круглость и false, если нет.

// setup test object
var testObj = {
    property_a:1, 
    property_b: {
        porperty_c: 2
        },
    property_d: {
        property_e: {
            property_f: 3
            } 
        }
    }

console.log(isCircularObject(testObj)); // false

// add reference to another node in the same object
testObj.property_d.property_e.property_g = testObj.property_b;
console.log(isCircularObject(testObj)); // false

// add circular node
testObj.property_b.property_c = testObj.property_b;
console.log(isCircularObject(testObj));  // true

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

Ответ 3

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

function findCircularObject(node, parents, tree){
    parents = parents || [];
    tree = tree || [];

    if (!node || typeof node != "object")
        return false;

    var keys = Object.keys(node), i, value;

    parents.push(node); // add self to current path
    for (i = keys.length - 1; i >= 0; i--){
        value = node[keys[i]];
        if (value && typeof value == "object") {
            tree.push(keys[i]);
            if (parents.indexOf(value) >= 0)
                return true;
            // check child nodes
            if (arguments.callee(value, parents, tree))
                return tree.join('.');
            tree.pop();
        }
    }
    parents.pop();
    return false;
}

Если вам не нужна строка, массив деревьев не нужен. Просто измените исходную функцию на

return value;

для самого кругового объекта или

return parents.pop();

для своего родителя.

Ответ 4

Я думал о том, что вы пытаетесь выполнить на основе исходного кода из вашего другого вопроса. Почему бы не сделать что-то подобное.

Player = function()
{
    this.UnitTypeXpower = 2
    this.UnitTypeYpower = 7

}

UnitTypeXAdd = function(owner)
{
    owner.UnitTypeXpower++;   
}

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

Ответ 5

Вот код, который я использую для обнаружения циклических ссылок, он использует технику, предложенную в принятом ответе asbjornu, посредством чего каждое значение проходит через и его ссылка поддерживается в массиве, так что следующее значение можно сравнить с предыдущим.

function isCircular(obj, arr) {
    "use strict";

    var type = typeof obj,
        propName,
        //keys,
        thisVal,
        //iterKeys,
        iterArr,
        lastArr;

    if (type !== "object" && type !== "function") {
        return false;
    }

    if (Object.prototype.toString.call(arr) !== '[object Array]') {
    //if (!Array.isArray(arr)) {
        type = typeof arr; // jslint sake
        if (!(type === "undefined" || arr === null)) {
            throw new TypeError("Expected attribute to be an array");
        }

        arr = [];
    }

    arr.push(obj);
    lastArr = arr.length - 1;

    for (propName in obj) {
    //keys = Object.keys(obj);
    //propName = keys[iterKeys];
    //for (iterKeys = keys.length - 1; iterKeys >= 0; iterKeys -= 1) {
        thisVal = obj[propName];
        //thisVal = obj[keys[iterKeys]];
        type = typeof thisVal;

        if (type === "object" || type === "function") {
            for (iterArr = lastArr; iterArr >= 0; iterArr -= 1) {
                if (thisVal === arr[iterArr]) {
                    return true;
                }
            }

            // alternative to the above for loop
            /*
            if (arr.indexOf(obj[propName]) >= 0) {
                return true;
            }
            */

            if (isCircular(thisVal, arr)) {
                return true;
            }

        }
    }

    arr.pop();

    return false;
}

Этот код доступен на jsfiddle, где вы можете проверить его самостоятельно. Я также провел некоторые тесты производительности на jsperf.

Array.indexOf был введен только с Javascript 1.6, см. страница MDN

Array.isArray был введен только с Javascript 1.8.5, см. страница MDN

Object.keys был введен только с Javascript 1.8.5, см. страница MDN

Также стоит отметить, что arguments.callee устарел и запрещен в строгом режиме, предпочитая использовать именованные функции