web-dev-qa-db-fra.com

JSON.stringify objets profonds

J'ai besoin d'une fonction qui construit une chaîne JSON valide à partir de n'importe quel argument, sauf:

  • éviter le problème de récursivité en ne ajoutant pas deux fois les objets
  • éviter le problème de la taille de la pile d'appels en tronquant au-delà d'une profondeur donnée

Généralement, il devrait être capable de traiter de gros objets, au prix de les tronquer.

En tant que référence, ce code échoue:

var json = JSON.stringify(window);

Eviter le problème de récursivité est assez simple:

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

Mais pour l'instant, à part copier et modifier le code de Douglas Crockford pour garder une trace de la profondeur, je n'ai trouvé aucun moyen d'éviter le débordement de pile sur des objets très profonds tels que window ou toute autre variable event. Y-a-t-il une solution simple ?

48
Denys Séguret

J'ai fait ce que j'avais au début craint de devoir faire: j'ai pris le code de Crockford et je l'ai modifié pour répondre à mes besoins. Maintenant, il construit JSON mais gère

  • cycles
  • objets trop profonds
  • tableaux trop longs
  • exceptions (accesseurs auxquels l'accès est interdit légalement)

Au cas où quelqu'un en aurait besoin, j'ai créé un référentiel GitHub: JSON.Prune sur GitHub

Voici le code:

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

}());

Un exemple de ce qui peut être fait:

var json = JSON.pruned(window);

Note: Contrairement au code de cette réponse, le référentiel GitHub est mis à jour si nécessaire (documentation, compatibilité, utilisation comme module dans les commonjs ou nœuds, sérialisations spécifiques, etc.). C'est une bonne idée de commencer par le référentiel si vous avez besoin de cette fonctionnalité d'élagage.

86
Denys Séguret

Si vous utilisez Node.js, vous pouvez utiliser util.inspect , qui prend un argument de profondeur.

11

Vous pouvez simplement utiliser une fonction Censor comme dans l'exemple ci-dessous:

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

La sortie est {"week":45,"month":7}.

Donc, comme pour votre exemple, vous devez retourner undefined si vous avez un objet de valeur, qui est une fenêtre.

4
Gábor Lipták

J'ai révisé la réponse de @ dystroy en ajoutant:

  • Indentation pour les sous-propriétés.
  • Une indication de l'endroit où les références circulaires pointent.
/**
 * 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);
};
4
Gili

Voici mon correcteur vers JSON dépouillé pour la journalisation en toute sécurité d'objets avec des références cycliques, des éléments DOM, des portées angulaires ou une fenêtre.

Empêche TypeError: Converting circular structure to JSON en remplaçant les références circulaires par ''.

Empêche RangeError: Maximum call stack size exceeded. Toutefois, il est recommandé d'utiliser maxDepth ou filterObjects de toute façon, car la sérialisation d'objets très profonds nécessite du temps et de l'espace, ce qui peut réduire sa convivialité pour la journalisation générale et même rendre le navigateur de test déconnecté lorsqu'il est utilisé dans des tests.

En option:

  • limite la profondeur d'inspection des objets (pas encore implémentée),
  • filtre les objets (tels que window, framework de test, test runner),
  • filtre les éléments DOM,
  • filtres objet angulaire $ attributs.

Source + commentaires: https://Gist.github.com/iki/9371373

0
iki

Je pense que le format que vous utilisez est juste impropre à faire ce que vous voulez. L'obtention de toutes les données contenues dans l'objet window dans une seule chaîne JSON suppose que vous gardiez cette chaîne en mémoire pendant que vous la construisez, ce qui provoque des problèmes que vous avez rencontrés.

Vous avez besoin d’un format qui vous permette d’envoyer des données lorsqu’elles sont analysées à partir de l’objet window afin de libérer de la mémoire à la volée. D'ailleurs, vous devriez utiliser quelque chose comme CSV, Text ou VarStream ( https://github.com/nfroidure/VarStream ).

Vous pouvez également itérer sur un objet et essayer de le faire en JSON.stringifiez-les en un ... essai. Si le test réussit, vous envoyez le fichier JSON. S'il échoue, vous parcourez les propriétés de l'objet avec le même essai ... catch etc ... Mais c'est une solution de rechange laide que je ne vous encourage pas à utiliser.

0
nfroidure