формат JSON.stringify deep объекты

мне нужна функция, строящая допустимую строку JSON из любого аргумента, но:

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

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

как ссылка, этот код не работает:

var json = JSON.stringify(window);

избежать проблемы рекурсивности достаточно просто :

var seen = [];
return JSON.stringify(o, function(_, value) {
    if (typeof value === 'object' && value !== null) {
        if (seen.indexOf(value) !== -1) return;
        else seen.push(value);
    }
    return value;
});

но пока, кроме копирования и изменения код Дугласа Крокфорда чтобы отслеживать глубину, я не нашел способа избежать переполнения стека на очень глубоких объектах, таких как window и event. Есть ли простое решение ?

8 ответов


Я сделал то, что я изначально боялся, что мне придется сделать : я взял код Крокфорда и изменил его для своих нужд. Теперь он строит JSON, но обрабатывает

  • циклы
  • слишком глубокие объекты
  • слишком длинные массивы
  • исключения (аксессоры, которые не могут быть легально доступны)

Если кому-то это нужно, я сделал репозиторий GitHub:в формате JSON.чернослив на GitHub

вот код :

// JSON.pruned : a function to stringify any object without overflow
// example : var json = JSON.pruned({a:'e', c:[1,2,{d:{e:42, f:'deep'}}]})
// two additional optional parameters :
//   - the maximal depth (default : 6)
//   - the maximal length of arrays (default : 50)
// GitHub : https://github.com/Canop/JSON.prune
// This is based on Douglas Crockford's code ( https://github.com/douglascrockford/JSON-js/blob/master/json2.js )
(function () {
    'use strict';

    var DEFAULT_MAX_DEPTH = 6;
    var DEFAULT_ARRAY_MAX_LENGTH = 50;
    var seen; // Same variable used for all stringifications

    Date.prototype.toPrunedJSON = Date.prototype.toJSON;
    String.prototype.toPrunedJSON = String.prototype.toJSON;

    var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
        escapable = /[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
        meta = {    // table of character substitutions
            '\b': '\b',
            '\t': '\t',
            '\n': '\n',
            '\f': '\f',
            '\r': '\r',
            '"' : '\"',
            '\': '\\'
        };

    function quote(string) {
        escapable.lastIndex = 0;
        return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
            var c = meta[a];
            return typeof c === 'string'
                ? c
                : '\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
        }) + '"' : '"' + string + '"';
    }

    function str(key, holder, depthDecr, arrayMaxLength) {
        var i,          // The loop counter.
            k,          // The member key.
            v,          // The member value.
            length,
            partial,
            value = holder[key];
        if (value && typeof value === 'object' && typeof value.toPrunedJSON === 'function') {
            value = value.toPrunedJSON(key);
        }

        switch (typeof value) {
        case 'string':
            return quote(value);
        case 'number':
            return isFinite(value) ? String(value) : 'null';
        case 'boolean':
        case 'null':
            return String(value);
        case 'object':
            if (!value) {
                return 'null';
            }
            if (depthDecr<=0 || seen.indexOf(value)!==-1) {
                return '"-pruned-"';
            }
            seen.push(value);
            partial = [];
            if (Object.prototype.toString.apply(value) === '[object Array]') {
                length = Math.min(value.length, arrayMaxLength);
                for (i = 0; i < length; i += 1) {
                    partial[i] = str(i, value, depthDecr-1, arrayMaxLength) || 'null';
                }
                v = partial.length === 0
                    ? '[]'
                    : '[' + partial.join(',') + ']';
                return v;
            }
            for (k in value) {
                if (Object.prototype.hasOwnProperty.call(value, k)) {
                    try {
                        v = str(k, value, depthDecr-1, arrayMaxLength);
                        if (v) partial.push(quote(k) + ':' + v);
                    } catch (e) { 
                        // this try/catch due to some "Accessing selectionEnd on an input element that cannot have a selection." on Chrome
                    }
                }
            }
            v = partial.length === 0
                ? '{}'
                : '{' + partial.join(',') + '}';
            return v;
        }
    }

    JSON.pruned = function (value, depthDecr, arrayMaxLength) {
        seen = [];
        depthDecr = depthDecr || DEFAULT_MAX_DEPTH;
        arrayMaxLength = arrayMaxLength || DEFAULT_ARRAY_MAX_LENGTH;
        return str('', {'': value}, depthDecr, arrayMaxLength);
    };

}());

An пример того, что можно сделать :

var json = JSON.pruned(window);

Примечание: вопреки коду в этом ответе,репозитории GitHub обновляется при необходимости (документация, совместимость, использование в качестве модуля в commonjs или узле, конкретные сериализации и т. д.). Это хорошая идея, чтобы начать с репозитория, если вам нужна эта функция обрезки.


Если вы используете Node.js вы можете использовать util.inspect, который принимает аргумент глубины.


Вы можете просто использовать Цензор НЕТ функции как в примере ниже:

function censor(key, value) {
  if (typeof(value) == "string") {
    return undefined;
  }
  return value;
}

var foo = {foundation: "Mozilla", model: "box", week: 45, transport: "car", month: 7};
var jsonString = JSON.stringify(foo, censor);

выход {"week":45,"month":7}.

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


Я пересмотрел ответ @dystroy, добавив:

  • отступ для вложенных свойств.
  • указание того, куда указывают круговые ссылки.
/**
 * Returns the JSON representation of an object.
 *
 * @param {value} object the object
 * @param {number} objectMaxDepth for objects, the maximum number of times to recurse into descendants
 * @param {number} arrayMaxLength for arrays, the maximum number of elements to enumerate
 * @param {string} indent the string to use for indentation
 * @return {string} the JSON representation
 */
var toJSON = function(object, objectMaxDepth, arrayMaxLength, indent)
{
    "use strict";

    /**
     * Escapes control characters, quote characters, backslash characters and quotes the string.
     *
     * @param {string} string the string to quote
     * @returns {String} the quoted string
     */
    function quote(string)
    {
        escapable.lastIndex = 0;
        var escaped;
        if (escapable.test(string))
        {
            escaped = string.replace(escapable, function(a)
            {
                var replacement = replacements[a];
                if (typeof (replacement) === "string")
                    return replacement;
                // Pad the unicode representation with leading zeros, up to 4 characters.
                return "\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4);
            });
        }
        else
            escaped = string;
        return "\"" + escaped + "\"";
    }

    /**
     * Returns the String representation of an object.
     * 
     * Based on <a href="https://github.com/Canop/JSON.prune/blob/master/JSON.prune.js">https://github.com/Canop/JSON.prune/blob/master/JSON.prune.js</a>
     *
     * @param {string} path the fully-qualified path of value in the JSON object
     * @param {type} value the value of the property
     * @param {string} cumulativeIndent the indentation to apply at this level
     * @param {number} depth the current recursion depth
     * @return {String} the JSON representation of the object, or "null" for values that aren't valid
     * in JSON (e.g. infinite numbers).
     */
    function toString(path, value, cumulativeIndent, depth)
    {
        switch (typeof (value))
        {
            case "string":
                return quote(value);
            case "number":
                {
                    // JSON numbers must be finite
                    if (isFinite(value))
                        return String(value);
                    return "null";
                }
            case "boolean":
                return String(value);
            case "object":
                {
                    if (!value)
                        return "null";
                    var valueIndex = values.indexOf(value);
                    if (valueIndex !== -1)
                        return "Reference => " + paths[valueIndex];
                    values.push(value);
                    paths.push(path);
                    if (depth > objectMaxDepth)
                        return "...";

                    // Make an array to hold the partial results of stringifying this object value.
                    var partial = [];

                    // Is the value an array?
                    var i;
                    if (Object.prototype.toString.apply(value) === "[object Array]")
                    {
                        // The value is an array. Stringify every element
                        var length = Math.min(value.length, arrayMaxLength);

                        // Whether a property has one or multiple values, they should be treated as the same
                        // object depth. As such, we do not increment the object depth when recursing into an
                        // array.
                        for (i = 0; i < length; ++i)
                        {
                            partial[i] = toString(path + "." + i, value[i], cumulativeIndent + indent, depth,
                                arrayMaxLength);
                        }
                        if (i < value.length)
                        {
                            // arrayMaxLength reached
                            partial[i] = "...";
                        }
                        return "\n" + cumulativeIndent + "[" + partial.join(", ") + "\n" + cumulativeIndent +
                            "]";
                    }

                    // Otherwise, iterate through all of the keys in the object.
                    for (var subKey in value)
                    {
                        if (Object.prototype.hasOwnProperty.call(value, subKey))
                        {
                            var subValue;
                            try
                            {
                                subValue = toString(path + "." + subKey, value[subKey], cumulativeIndent + indent,
                                    depth + 1);
                                partial.push(quote(subKey) + ": " + subValue);
                            }
                            catch (e)
                            {
                                // this try/catch due to forbidden accessors on some objects
                                if (e.message)
                                    subKey = e.message;
                                else
                                    subKey = "access denied";
                            }
                        }
                    }
                    var result = "\n" + cumulativeIndent + "{\n";
                    for (i = 0; i < partial.length; ++i)
                        result += cumulativeIndent + indent + partial[i] + ",\n";
                    if (partial.length > 0)
                    {
                        // Remove trailing comma
                        result = result.slice(0, result.length - 2) + "\n";
                    }
                    result += cumulativeIndent + "}";
                    return result;
                }
            default:
                return "null";
        }
    }

    if (indent === undefined)
        indent = "  ";
    if (objectMaxDepth === undefined)
        objectMaxDepth = 0;
    if (arrayMaxLength === undefined)
        arrayMaxLength = 50;
    // Matches characters that must be escaped
    var escapable =
        /[\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
    // The replacement characters
    var replacements =
        {
            "\b": "\b",
            "\t": "\t",
            "\n": "\n",
            "\f": "\f",
            "\r": "\r",
            "\"": "\\"",
            "\": "\\"
        };
    // A list of all the objects that were seen (used to avoid recursion)
    var values = [];
    // The path of an object in the JSON object, with indexes corresponding to entries in the
    // "values" variable.
    var paths = [];
    return toString("root", object, "", 0);
};

Я думаю, что формат, который вы используете, просто не подходит для того, чтобы делать то, что вы хотите. Получение всех данных, содержащихся в объекте window в одной строке JSON предположим, что вы держите эту строку в памяти во время ее создания, вызывая проблемы, с которыми вы столкнулись.

вам нужен формат, дающий вам возможность отправлять данные по мере их анализа из объекта window, чтобы освободить память на лету. Если на то пошло, вы должны использовать что-то вроде CSV, Text или VarStream ( https://github.com/nfroidure/VarStream ).

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


вот мой stringifier для удаления JSON для безопасного ведения журнала объектов с циклическими ссылками, элементами DOM, угловыми областями или окном.

предупреждает TypeError: Converting circular structure to JSON путем замены круговых ссылок на ".

предупреждает RangeError: Maximum call stack size exceeded. Тем не менее, рекомендуется использовать maxDepth или filterObjects в любом случае, потому что сериализация очень глубоких объектов стоит как времени, так и пространства, что может снизить его удобство для общего ведения журнала и даже отключить тестовый браузер, когда используется в тестах.

дополнительно:

  • ограничивает глубину осмотра объекта (еще не реализовано),
  • фильтрует объекты (например, окно, тестовая среда, тестовый бегун),
  • фильтры DOM-элементами,
  • фильтры угловой объект $атрибуты.

Источник+Комментарии: https://gist.github.com/iki/9371373


(function (input, level) {
    if (!input)
        return input;

    level = level || 4;

    var objectsAlreadySerialized = [input],
        objDepth = [input];

    return JSON.stringify(input, function (key, value) {
        if (key) {
            if (typeof value === 'object') {
                if (objectsAlreadySerialized.indexOf(value) !== -1)
                    return undefined;

                objectsAlreadySerialized.push(value);
            }

            if (objDepth.indexOf(this) === -1)
                objDepth.push(this);
            else while(objDepth[objDepth.length-1] !== this)
                objDepth.pop();

            if (objDepth.length > level)
                return undefined;
        }

        return value;
    });
})(window, 6)

вы можете просто сохранить глубину, на которой находитесь:

function stringify(obj, currentDepth, maxDepth) {
  if (currentDepth == maxDepth) return '[Warning: max level reached]'
  var str = '{';
  for (var key in obj) {
    str += key + ': ' + typeof obj == 'object' ?
        stringify(obj[key], currentDepth + 1, maxDepth) :
        obj[key];
  }
  return str + '}'
}

(просто пример - очевидно, этот фрагмент не обнаружить рекурсию)