La forma más rápida de aplanar/desaplanar objetos JSON anidados

15 minutos de lectura

La forma mas rapida de aplanardesaplanar objetos JSON anidados
Luis Ricci

Junté algo de código para aplanar y desacoplar objetos JSON complejos/anidados. Funciona, pero es un poco lento (activa la advertencia de ‘secuencia de comandos larga’).

Para los nombres aplanados quiero “.” como delimitador y [INDEX] para matrices.

Ejemplos:

un-flattened | flattened
---------------------------
{foo:{bar:false}} => {"foo.bar":false}
{a:[{b:["c","d"]}]} => {"a[0].b[0]":"c","a[0].b[1]":"d"}
[1,[2,[3,4],5],6] => {"[0]":1,"[1].[0]":2,"[1].[1].[0]":3,"[1].[1].[1]":4,"[1].[2]":5,"[2]":6}

Creé un punto de referencia que ~ simula mi caso de uso http://jsfiddle.net/WSzec/

  • Obtener un objeto JSON anidado
  • aplanarlo
  • Mírelo y posiblemente modifíquelo mientras está aplanado
  • Vuelva a descomponerlo a su formato anidado original para enviarlo

Me gustaría un código más rápido: para aclarar, el código que completa el punto de referencia JSFiddle (http://jsfiddle.net/WSzec/) significativamente más rápido (~20%+ estaría bien) en IE 9+, FF 24+ y Chrome 29+.

Aquí está el código JavaScript relevante: Actual más rápido: http://jsfiddle.net/WSzec/6/

JSON.unflatten = function(data) {
    "use strict";
    if (Object(data) !== data || Array.isArray(data))
        return data;
    var result = {}, cur, prop, idx, last, temp;
    for(var p in data) {
        cur = result, prop = "", last = 0;
        do {
            idx = p.indexOf(".", last);
            temp = p.substring(last, idx !== -1 ? idx : undefined);
            cur = cur[prop] || (cur[prop] = (!isNaN(parseInt(temp)) ? [] : {}));
            prop = temp;
            last = idx + 1;
        } while(idx >= 0);
        cur[prop] = data[p];
    }
    return result[""];
}
JSON.flatten = function(data) {
    var result = {};
    function recurse (cur, prop) {
        if (Object(cur) !== cur) {
            result[prop] = cur;
        } else if (Array.isArray(cur)) {
             for(var i=0, l=cur.length; i<l; i++)
                 recurse(cur[i], prop ? prop+"."+i : ""+i);
            if (l == 0)
                result[prop] = [];
        } else {
            var isEmpty = true;
            for (var p in cur) {
                isEmpty = false;
                recurse(cur[p], prop ? prop+"."+p : p);
            }
            if (isEmpty)
                result[prop] = {};
        }
    }
    recurse(data, "");
    return result;
}

EDITAR 1 Modificó lo anterior a la implementación de @Bergi, que actualmente es la más rápida. Aparte, usar “.indexOf” en lugar de “regex.exec” es un 20% más rápido en FF pero un 20% más lento en Chrome; así que me quedaré con la expresión regular ya que es más simple (aquí está mi intento de usar indexOf para reemplazar la expresión regular http://jsfiddle.net/WSzec/2/).

EDITAR 2 Sobre la base de la idea de @Bergi, logré crear una versión no regular más rápida (3 veces más rápida en FF y ~ 10% más rápida en Chrome). http://jsfiddle.net/WSzec/6/ En esta implementación (la actual), las reglas para los nombres de clave son simples, las claves no pueden comenzar con un número entero ni contener un punto.

Ejemplo:

  • {“foo”:{“barra”:[0]}} => {“foo.bar.0”:0}

EDITAR 3 Agregar el enfoque de análisis de ruta en línea de @AaditMShah (en lugar de String.split) ayudó a mejorar el rendimiento sin aplanar. Estoy muy contento con la mejora general del rendimiento alcanzada.

Los últimos jsfiddle y jsperf:

http://jsfiddle.net/WSzec/14/

http://jsperf.com/flatten-un-flatten/4

  • No existe tal cosa como un “objeto JSON”. La pregunta parece ser sobre objetos JS.

    – Félix Kling

    30 de septiembre de 2013 a las 16:08


  • Esta pregunta parece ser más apropiada para el sitio Code Review StackExchange: codereview.stackexchange.com

    – Aadit M Shah

    30 de septiembre de 2013 a las 16:17

  • @FelixKling: por objeto JSON me refería a objetos JS que solo contienen tipos primitivos de JavaScript. Podría, por ejemplo, poner una función en un objeto JS, pero no se serializaría en JSON, es decir, JSON.stringify({fn:function(){alert(‘a’);}}); —

    – Luis Ricci

    30 de septiembre de 2013 a las 16:19

  • [1].[1].[0] me parece mal ¿Estás seguro de que este es el resultado deseado?

    – Bergi

    30 de septiembre de 2013 a las 16:25

  • Lamentablemente, hay un error: los objetos de fecha se convierten en un JSON vacío.

    – giacecco

    28 de marzo de 2016 a las 15:09


1646967553 454 La forma mas rapida de aplanardesaplanar objetos JSON anidados
Bergi

Aquí está mi implementación mucho más corta:

Object.unflatten = function(data) {
    "use strict";
    if (Object(data) !== data || Array.isArray(data))
        return data;
    var regex = /\.?([^.\[\]]+)|\[(\d+)\]/g,
        resultholder = {};
    for (var p in data) {
        var cur = resultholder,
            prop = "",
            m;
        while (m = regex.exec(p)) {
            cur = cur[prop] || (cur[prop] = (m[2] ? [] : {}));
            prop = m[2] || m[1];
        }
        cur[prop] = data[p];
    }
    return resultholder[""] || resultholder;
};

flatten no ha cambiado mucho (y no estoy seguro de si realmente necesita esos isEmpty casos):

Object.flatten = function(data) {
    var result = {};
    function recurse (cur, prop) {
        if (Object(cur) !== cur) {
            result[prop] = cur;
        } else if (Array.isArray(cur)) {
             for(var i=0, l=cur.length; i<l; i++)
                 recurse(cur[i], prop + "[" + i + "]");
            if (l == 0)
                result[prop] = [];
        } else {
            var isEmpty = true;
            for (var p in cur) {
                isEmpty = false;
                recurse(cur[p], prop ? prop+"."+p : p);
            }
            if (isEmpty && prop)
                result[prop] = {};
        }
    }
    recurse(data, "");
    return result;
}

Juntos, ellos ejecuta tu punto de referencia en aproximadamente la mitad del tiempo (Opera 12.16: ~900ms en lugar de ~1900ms, Chrome 29: ~800ms en lugar de ~1600ms).

Nota: Esta y la mayoría de las otras soluciones respondidas aquí se centran en la velocidad y son susceptibles a prototipo de contaminación y no debe usarse en objetos que no sean de confianza.

  • ¡Esto es genial! La expresión regular funciona notablemente bien (especialmente en Chrome), intenté reemplazarla con la lógica indexOf, pero solo pude acelerar en FF. Agregaré una recompensa a esta pregunta para ver si se puede generar otra mejora inteligente, pero hasta ahora esto es más de lo que esperaba.

    – Luis Ricci

    1 oct 2013 a las 20:50

  • Logré obtener más velocidad de su implementación reemplazando regex.exec() con string.split() y simplificando el formato de la clave. Le daré unos días antes de otorgarle los puntos, pero creo que se ha alcanzado el ‘muro de optimización significativa’.

    – Luis Ricci

    5/10/2013 a las 21:52

  • JSON.flatten({}); // { ”: {} } — puede agregar una línea después de var result = {}; — if (resultado === datos) devuelve datos;

    – Iván

    29 noviembre 2013 a las 21:20


  • @Ivan: Ah, gracias por ese caso extremo, aunque semánticamente sería necesario tener una representación adicional para objetos vacíos. Pero no, result === data no funcionará, nunca son idénticos.

    – Bergi

    30 de noviembre de 2013 a las 12:29


  • @Bergi Sí, tienes razón. Aunque Object.keys(data).length === 0 funciona

    – Iván

    5 de diciembre de 2013 a las 1:43

Escribí dos funciones para flatten y unflatten un objeto JSON.


Aplanar un objeto JSON:

var flatten = (function (isArray, wrapped) {
    return function (table) {
        return reduce("", {}, table);
    };

    function reduce(path, accumulator, table) {
        if (isArray(table)) {
            var length = table.length;

            if (length) {
                var index = 0;

                while (index < length) {
                    var property = path + "[" + index + "]", item = table[index++];
                    if (wrapped(item) !== item) accumulator[property] = item;
                    else reduce(property, accumulator, item);
                }
            } else accumulator[path] = table;
        } else {
            var empty = true;

            if (path) {
                for (var property in table) {
                    var item = table[property], property = path + "." + property, empty = false;
                    if (wrapped(item) !== item) accumulator[property] = item;
                    else reduce(property, accumulator, item);
                }
            } else {
                for (var property in table) {
                    var item = table[property], empty = false;
                    if (wrapped(item) !== item) accumulator[property] = item;
                    else reduce(property, accumulator, item);
                }
            }

            if (empty) accumulator[path] = table;
        }

        return accumulator;
    }
}(Array.isArray, Object));

Rendimiento:

  1. Es más rápido que la solución actual en Opera. La solución actual es un 26 % más lenta en Opera.
  2. Es más rápido que la solución actual en Firefox. La solución actual es un 9% más lenta en Firefox.
  3. Es más rápido que la solución actual en Chrome. La solución actual es un 29 % más lenta en Chrome.

Descomprimir un objeto JSON:

function unflatten(table) {
    var result = {};

    for (var path in table) {
        var cursor = result, length = path.length, property = "", index = 0;

        while (index < length) {
            var char = path.charAt(index);

            if (char === "[") {
                var start = index + 1,
                    end = path.indexOf("]", start),
                    cursor = cursor[property] = cursor[property] || [],
                    property = path.slice(start, end),
                    index = end + 1;
            } else {
                var cursor = cursor[property] = cursor[property] || {},
                    start = char === "." ? index + 1 : index,
                    bracket = path.indexOf("[", start),
                    dot = path.indexOf(".", start);

                if (bracket < 0 && dot < 0) var end = index = length;
                else if (bracket < 0) var end = index = dot;
                else if (dot < 0) var end = index = bracket;
                else var end = index = bracket < dot ? bracket : dot;

                var property = path.slice(start, end);
            }
        }

        cursor[property] = table[path];
    }

    return result[""];
}

Rendimiento:

  1. Es más rápido que la solución actual en Opera. La solución actual es un 5% más lenta en Opera.
  2. Es más lento que la solución actual en Firefox. Mi solución es un 26% más lenta en Firefox.
  3. Es más lento que la solución actual en Chrome. Mi solución es un 6% más lenta en Chrome.

Aplanar y desacoplar un objeto JSON:

En general, mi solución funciona igual de bien o incluso mejor que la solución actual.

Rendimiento:

  1. Es más rápido que la solución actual en Opera. La solución actual es un 21 % más lenta en Opera.
  2. Es tan rápido como la solución actual en Firefox.
  3. Es más rápido que la solución actual en Firefox. La solución actual es un 20 % más lenta en Chrome.

Formato de salida:

Un objeto aplanado usa la notación de puntos para las propiedades del objeto y la notación de corchetes para los índices de matriz:

  1. {foo:{bar:false}} => {"foo.bar":false}
  2. {a:[{b:["c","d"]}]} => {"a[0].b[0]":"c","a[0].b[1]":"d"}
  3. [1,[2,[3,4],5],6] => {"[0]":1,"[1][0]":2,"[1][1][0]":3,"[1][1][1]":4,"[1][2]":5,"[2]":6}

En mi opinión, este formato es mejor que usar solo la notación de puntos:

  1. {foo:{bar:false}} => {"foo.bar":false}
  2. {a:[{b:["c","d"]}]} => {"a.0.b.0":"c","a.0.b.1":"d"}
  3. [1,[2,[3,4],5],6] => {"0":1,"1.0":2,"1.1.0":3,"1.1.1":4,"1.2":5,"2":6}

Ventajas:

  1. Aplanar un objeto es más rápido que la solución actual.
  2. Aplanar y aplanar un objeto es tan rápido o más rápido que la solución actual.
  3. Los objetos aplanados utilizan tanto la notación de puntos como la notación de corchetes para mejorar la legibilidad.

Desventajas:

  1. Descomponer un objeto es más lento que la solución actual en la mayoría de los casos (pero no en todos).

La corriente demostración de JSFiddle dio los siguientes valores como salida:

Nested : 132175 : 63
Flattened : 132175 : 564
Nested : 132175 : 54
Flattened : 132175 : 508

mi actualizado demostración de JSFiddle dio los siguientes valores como salida:

Nested : 132175 : 59
Flattened : 132175 : 514
Nested : 132175 : 60
Flattened : 132175 : 451

No estoy muy seguro de lo que eso significa, así que me quedaré con los resultados de jsPerf. Después de todo, jsPerf es una utilidad de evaluación comparativa de rendimiento. JSFiddle no lo es.

  • Muy genial. Realmente me gusta el estilo para aplanar, usando funciones anónimas para obtener Array.isArray y Object en un alcance más cercano. Sin embargo, creo que el objeto de prueba que está usando para la prueba JSPerf es demasiado simple. Creé el objeto “fillObj({},4)” en mi punto de referencia jsfiddle para emular un caso real de una gran pieza compleja de datos anidados.

    – Luis Ricci

    6 oct 2013 a las 15:04

  • Muéstrame el código de tu objeto y lo incorporaré al benchmark.

    – Aadit M Shah

    6 oct 2013 a las 15:08

  • @LastCoder Hmmm, su implementación actual parece ser más rápida que la mía en la mayoría de los navegadores (especialmente Firefox). Curiosamente, mi implementación es más rápida en Opera y tampoco es tan mala en Chrome. No creo que tener un conjunto de datos tan grande sea un factor ideal para determinar la velocidad del algoritmo porque: 1) los conjuntos de datos grandes necesitan una gran cantidad de memoria, intercambio de páginas, etc.; y eso no es algo que pueda controlar en JS (es decir, está a merced del navegador) 2) si desea hacer un trabajo intensivo de CPU, entonces JS no es el mejor lenguaje. Considere usar C en su lugar. Hay bibliotecas JSON para C

    – Aadit M Shah

    6 oct 2013 a las 16:51


  • ese es un buen punto y trae a colación la diferencia entre el benchmarking sintético y el real. Estoy contento con el rendimiento del JS optimizado actual, por lo que no es necesario usar C.

    – Luis Ricci

    7 oct 2013 a las 13:54

  • Esta implementación también tiene un prototipo de error de contaminación, por ejemplo unflatten({"foo.__proto__.bar": 42})

    – Alex Brasetvik

    19 de febrero de 2020 a las 14:06

1646967553 105 La forma mas rapida de aplanardesaplanar objetos JSON anidados
Chico

Versión ES6:

const flatten = (obj, path="") => {        
    if (!(obj instanceof Object)) return {[path.replace(/\.$/g, '')]:obj};

    return Object.keys(obj).reduce((output, key) => {
        return obj instanceof Array ? 
             {...output, ...flatten(obj[key], path +  '[' + key + '].')}:
             {...output, ...flatten(obj[key], path + key + '.')};
    }, {});
}

Ejemplo:

console.log(flatten({a:[{b:["c","d"]}]}));
console.log(flatten([1,[2,[3,4],5],6]));

  • Creo que tendría algunas dificultades para UNflattening si no tiene separadores entre los nombres de propiedad JSON.stringify(flatten({“prop1″:0,”prop2”:{“prop3″:true,”prop4″:”test”}})); ==> {“prop1″:0,”prop2prop3″:verdadero,”prop2prop4″:”prueba”} pero es una solución fácil, la brevedad de la sintaxis de ES6 es realmente agradable

    – Luis Ricci

    1 de marzo de 2018 a las 19:44


  • Esto no funciona bien con Date, ¿alguna idea de cómo hacer que haga eso? Por ejemplo, con flatten({a: {b: new Date()}});

    –Ehtesh Choudhury

    22 de septiembre de 2018 a las 2:01

  • Puede usar marcas de tiempo: {b: nueva fecha (). getTime ()}} y luego devolverlo a la fecha con nueva fecha (marca de tiempo)

    – Chico

    22/09/2018 a las 13:35


  • Esto es terriblemente lento.

    – Jorge Fuentes González

    22 de septiembre de 2020 a las 14:56

3 años y medio después…

Para mi propio proyecto, quería aplanar objetos JSON en notación de punto mongoDB y se le ocurrió una solución simple:

/**
 * Recursively flattens a JSON object using dot notation.
 *
 * NOTE: input must be an object as described by JSON spec. Arbitrary
 * JS objects (e.g. {a: () => 42}) may result in unexpected output.
 * MOREOVER, it removes keys with empty objects/arrays as value (see
 * examples bellow).
 *
 * @example
 * // returns {a:1, 'b.0.c': 2, 'b.0.d.e': 3, 'b.1': 4}
 * flatten({a: 1, b: [{c: 2, d: {e: 3}}, 4]})
 * // returns {a:1, 'b.0.c': 2, 'b.0.d.e.0': true, 'b.0.d.e.1': false, 'b.0.d.e.2.f': 1}
 * flatten({a: 1, b: [{c: 2, d: {e: [true, false, {f: 1}]}}]})
 * // return {a: 1}
 * flatten({a: 1, b: [], c: {}})
 *
 * @param obj item to be flattened
 * @param {Array.string} [prefix=[]] chain of prefix joined with a dot and prepended to key
 * @param {Object} [current={}] result of flatten during the recursion
 *
 * @see https://docs.mongodb.com/manual/core/document/#dot-notation
 */
function flatten (obj, prefix, current) {
  prefix = prefix || []
  current = current || {}

  // Remember kids, null is also an object!
  if (typeof (obj) === 'object' && obj !== null) {
    Object.keys(obj).forEach(key => {
      this.flatten(obj[key], prefix.concat(key), current)
    })
  } else {
    current[prefix.join('.')] = obj
  }

  return current
}

Características y/o advertencias

  • Solo acepta objetos JSON. Así que si pasas algo como {a: () => {}} ¡Puede que no consigas lo que querías!
  • Elimina matrices y objetos vacíos. Así que esto {a: {}, b: []} se aplana a {}.

1646967553 454 La forma mas rapida de aplanardesaplanar objetos JSON anidados
Bergi

Aquí hay otro enfoque que funciona más lento (alrededor de 1000 ms) que la respuesta anterior, pero tiene una idea interesante 🙂

En lugar de iterar a través de cada cadena de propiedades, simplemente elige la última propiedad y usa una tabla de búsqueda para el resto para almacenar los resultados intermedios. Esta tabla de búsqueda se repetirá hasta que no queden cadenas de propiedades y todos los valores residan en propiedades no cocatenadas.

JSON.unflatten = function(data) {
    "use strict";
    if (Object(data) !== data || Array.isArray(data))
        return data;
    var regex = /\.?([^.\[\]]+)$|\[(\d+)\]$/,
        props = Object.keys(data),
        result, p;
    while(p = props.shift()) {
        var m = regex.exec(p),
            target;
        if (m.index) {
            var rest = p.slice(0, m.index);
            if (!(rest in data)) {
                data[rest] = m[2] ? [] : {};
                props.push(rest);
            }
            target = data[rest];
        } else {
            target = result || (result = (m[2] ? [] : {}));
        }
        target[m[2] || m[1]] = data[p];
    }
    return result;
};

Actualmente utiliza el data parámetro de entrada para la tabla, y le da muchas propiedades; también debería ser posible una versión no destructiva. Tal vez un inteligente lastIndexOf el uso funciona mejor que la expresión regular (depende del motor de expresión regular).

Véalo en acción aquí.

  • No voté negativamente tu respuesta. Sin embargo, me gustaría señalar que su función no unflatten correctamente el objeto aplanado. Por ejemplo, considere la matriz [1,[2,[3,4],5],6]. Tu flatten La función aplana este objeto para {"[0]":1,"[1][0]":2,"[1][1][0]":3,"[1][1][1]":4,"[1][2]":5,"[2]":6}. Tu unflatten Sin embargo, la función desinfla incorrectamente el objeto aplanado para [1,[null,[3,4]],6]. La razón por la que esto sucede es por la declaración delete data[p] que borra prematuramente el valor intermedio [2,null,5] antes de [3,4] se le agrega. Usa una pila para resolverlo. 🙂

    – Aadit M Shah

    06/10/2013 a las 16:30


  • Ah, ya veo, orden de enumeración indefinido… Voy a arreglarlo con una cola de propiedades, ponga su solución de pila en una respuesta propia. ¡Gracias por la pista!

    – Bergi

    06/10/2013 a las 18:22

Puedes usar https://github.com/hughsk/plano

Tome un objeto Javascript anidado y aplánelo, o descomprima un objeto con claves delimitadas.

Ejemplo del documento

var flatten = require('flat')

flatten({
    key1: {
        keyA: 'valueI'
    },
    key2: {
        keyB: 'valueII'
    },
    key3: { a: { b: { c: 2 } } }
})

// {
//   'key1.keyA': 'valueI',
//   'key2.keyB': 'valueII',
//   'key3.a.b.c': 2
// }


var unflatten = require('flat').unflatten

unflatten({
    'three.levels.deep': 42,
    'three.levels': {
        nested: true
    }
})

// {
//     three: {
//         levels: {
//             deep: 42,
//             nested: true
//         }
//     }
// }

  • No voté negativamente tu respuesta. Sin embargo, me gustaría señalar que su función no unflatten correctamente el objeto aplanado. Por ejemplo, considere la matriz [1,[2,[3,4],5],6]. Tu flatten La función aplana este objeto para {"[0]":1,"[1][0]":2,"[1][1][0]":3,"[1][1][1]":4,"[1][2]":5,"[2]":6}. Tu unflatten Sin embargo, la función desinfla incorrectamente el objeto aplanado para [1,[null,[3,4]],6]. La razón por la que esto sucede es por la declaración delete data[p] que borra prematuramente el valor intermedio [2,null,5] antes de [3,4] se le agrega. Usa una pila para resolverlo. 🙂

    – Aadit M Shah

    06/10/2013 a las 16:30


  • Ah, ya veo, orden de enumeración indefinido… Voy a arreglarlo con una cola de propiedades, ponga su solución de pila en una respuesta propia. ¡Gracias por la pista!

    – Bergi

    06/10/2013 a las 18:22

Utilice esta biblioteca:

npm install flat

uso (desde https://www.npmjs.com/package/flat):

Aplanar:

    var flatten = require('flat')


    flatten({
        key1: {
            keyA: 'valueI'
        },
        key2: {
            keyB: 'valueII'
        },
        key3: { a: { b: { c: 2 } } }
    })

    // {
    //   'key1.keyA': 'valueI',
    //   'key2.keyB': 'valueII',
    //   'key3.a.b.c': 2
    // }

Des-aplanar:

var unflatten = require('flat').unflatten

unflatten({
    'three.levels.deep': 42,
    'three.levels': {
        nested: true
    }
})

// {
//     three: {
//         levels: {
//             deep: 42,
//             nested: true
//         }
//     }
// }

  • Para completar su respuesta, debe agregar un ejemplo de cómo usar esa biblioteca.

    – Antonio Almeida

    23 de julio de 2019 a las 11:21

  • Parece ser un módulo legítimo. Descargas semanales de NPM: 3.812.119 (!!!) y actualizaciones periódicas. Gracias por el aviso.

    – Sean Rasmussen

    17 oct 2020 a las 11:18

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad