Iterar a través de objetos JavaScript anidados [duplicado]

Resuelto NewToThis asked hace 12 años • 15 respuestas

Estoy intentando recorrer un objeto anidado para recuperar un objeto específico identificado por una cadena. En el objeto de ejemplo siguiente, la cadena de identificación es la propiedad "etiqueta". No puedo entender cómo iterar hacia abajo a través del árbol para devolver el objeto apropiado. Cualquier ayuda o sugerencia será muy apreciada.

var cars = {
  label: 'Autos',
  subs: [
    {
      label: 'SUVs',
      subs: []
    },
    {
      label: 'Trucks',
      subs: [
        {
          label: '2 Wheel Drive',
          subs: []
        },
        {
          label: '4 Wheel Drive',
          subs: [
            {
              label: 'Ford',
              subs: []
            },
            {
              label: 'Chevrolet',
              subs: []
            }
          ]
        }
      ]
    },
    {
      label: 'Sedan',
      subs: []
    }
  ]
}
NewToThis avatar Nov 11 '11 02:11 NewToThis
Aceptado

Actualización 2023:

Mucha gente ha preguntado sobre el enfoque no recursivo , así que aquí está:

const iterate = (obj) => {
  const stack = [obj];
  while (stack?.length > 0) {
    const currentObj = stack.pop();
    Object.keys(currentObj).forEach(key => {
      console.log(`key: ${key}, value: ${currentObj[key]}`);
      if (typeof currentObj[key] === 'object' && currentObj[key] !== null) {
        stack.push(currentObj[key]);
      }
    });
  }
};

Ejemplo de REPL

Respuesta original con recursividad

En caso de que desee iterar profundamente en un objeto complejo (anidado) para cada clave y valor , puede hacerlo usando Object.keys() , de forma recursiva :

const iterate = (obj) => {
    Object.keys(obj).forEach(key => {

    console.log(`key: ${key}, value: ${obj[key]}`)

    if (typeof obj[key] === 'object' && obj[key] !== null) {
            iterate(obj[key])
        }
    })
}

Ejemplo de REPL

Menelaos Kotsollaris avatar Jan 20 '2019 00:01 Menelaos Kotsollaris

Puede crear una función recursiva como esta para realizar un recorrido en profundidad del carsobjeto.

var findObjectByLabel = function(obj, label) {
    if(obj.label === label) { return obj; }
    for(var i in obj) {
        if(obj.hasOwnProperty(i)){
            var foundLabel = findObjectByLabel(obj[i], label);
            if(foundLabel) { return foundLabel; }
        }
    }
    return null;
};

que se puede llamar así

findObjectByLabel(car, "Chevrolet");
Peter Olson avatar Nov 10 '2011 19:11 Peter Olson

𝗗𝗲𝗮𝗱-𝘀𝗶𝗺𝗽𝗹𝗲 𝘄𝗶𝘁𝗵 𝟯 𝘃𝗮𝗿𝗶𝗮𝗯𝗹𝗲𝘀, 𝟵 𝗹𝗶𝗻𝗲 𝘀, 𝗮𝗻𝗱 𝗻𝗼 𝗿𝗲𝗰𝘂𝗿𝘀𝗶𝗼𝗻

function forEachNested(O, f, cur){
    O = [ O ]; // ensure that f is called with the top-level object
    while (O.length) // keep on processing the top item on the stack
        if(
           !f( cur = O.pop() ) && // do not spider down if `f` returns true
           cur instanceof Object && // ensure cur is an object, but not null 
           [Object, Array].includes(cur.constructor) //limit search to [] and {}
        ) O.push.apply(O, Object.values(cur)); //search all values deeper inside
}

Para utilizar la función anterior, pase la matriz como primer argumento y la función de devolución de llamada como segundo argumento. La función de devolución de llamada recibirá 1 argumento cuando se llame: el elemento actual que se está iterando.

(function(){"use strict";

var cars = {"label":"Autos","subs":[{"label":"SUVs","subs":[]},{"label":"Trucks","subs":[{"label":"2 Wheel Drive","subs":[]},{"label":"4 Wheel Drive","subs":[{"label":"Ford","subs":[]},{"label":"Chevrolet","subs":[]}]}]},{"label":"Sedan","subs":[]}]};

var lookForCar = prompt("enter the name of the car you are looking for (e.g. 'Ford')") || 'Ford';
lookForCar = lookForCar.replace(/[^ \w]/g, ""); // incaseif the user put quotes or something around their input
lookForCar = lookForCar.toLowerCase();

var foundObject = null;
forEachNested(cars, function(currentValue){
    if(currentValue.constructor === Object &&
      currentValue.label.toLowerCase() === lookForCar) {
        foundObject = currentValue;
    }
});
if (foundObject !== null) {
    console.log("Found the object: " + JSON.stringify(foundObject, null, "\t"));
} else {
    console.log('Nothing found with a label of "' + lookForCar + '" :(');
}

function forEachNested(O, f, cur){
    O = [ O ]; // ensure that f is called with the top-level object
    while (O.length) // keep on processing the top item on the stack
        if(
           !f( cur = O.pop() ) && // do not spider down if `f` returns true
           cur instanceof Object && // ensure cur is an object, but not null 
           [Object, Array].includes(cur.constructor) //limit search to [] and {}
        ) O.push.apply(O, Object.values(cur)); //search all values deeper inside
}

})();
Expandir fragmento

Una alternativa de "trampa" podría ser utilizar JSON.stringifypara iterar. SIN EMBARGO, JSON.stringifyllamará al toStringmétodo de cada objeto que pase por alto, lo que puede producir resultados no deseados si tiene sus propios usos especiales para toString.

function forEachNested(O, f, v){
    typeof O === "function" ? O(v) : JSON.stringify(O,forEachNested.bind(0,f));
    return v; // so that JSON.stringify keeps on recursing
}

(function(){"use strict";

var cars = {"label":"Autos","subs":[{"label":"SUVs","subs":[]},{"label":"Trucks","subs":[{"label":"2 Wheel Drive","subs":[]},{"label":"4 Wheel Drive","subs":[{"label":"Ford","subs":[]},{"label":"Chevrolet","subs":[]}]}]},{"label":"Sedan","subs":[]}]};

var lookForCar = prompt("enter the name of the car you are looking for (e.g. 'Ford')") || 'Ford';
lookForCar = lookForCar.replace(/[^ \w]/g, ""); // incaseif the user put quotes or something around their input
lookForCar = lookForCar.toLowerCase();

var foundObject = null;
forEachNested(cars, function(currentValue){
    if(currentValue.constructor === Object &&
      currentValue.label.toLowerCase() === lookForCar) {
        foundObject = currentValue;
    }
});
if (foundObject !== null)
    console.log("Found the object: " + JSON.stringify(foundObject, null, "\t"));
else
    console.log('Nothing found with a label of "' + lookForCar + '" :(');

function forEachNested(O, f, v){
    typeof O === "function" ? O(v) : JSON.stringify(O,forEachNested.bind(0,f));
    return v; // so that JSON.stringify keeps on recursing
}
})();
Expandir fragmento

Sin embargo, si bien el método anterior puede ser útil para fines de demostración, Object.valuesInternet Explorer no lo admite y hay muchos lugares en el código que presentan un rendimiento terriblemente deficiente:

  1. el código cambia el valor de los parámetros de entrada (argumentos) [líneas 2 y 5],
  2. el código llama Array.prototype.pushy Array.prototype.popen cada elemento [líneas 5 y 8],
  3. el código solo hace una comparación de punteros para el constructor que no funciona en objetos fuera de la ventana [línea 7],
  4. el código duplica la matriz devuelta desde Object.values[línea 8],
  5. el código no localiza window.Objecto window.Object.values[línea 9],
  6. y el código llama innecesariamente a Object.values ​​en matrices [línea 8].

A continuación se muestra una versión mucho más rápida que debería ser mucho más rápida que cualquier otra solución. La siguiente solución soluciona todos los problemas de rendimiento enumerados anteriormente. Sin embargo, itera de una manera muy diferente: primero itera todas las matrices y luego itera todos los objetos. Continúa iterando su tipo actual hasta que se agota por completo, incluidos los subvalores de iteración dentro de la lista actual del tipo actual que se está iterando. Luego, la función itera todos los demás tipos. Al iterar hasta el agotamiento antes de cambiar, el ciclo de iteración se calienta más que de otra manera y se itera aún más rápido. Este método también viene con una ventaja adicional: a la devolución de llamada que se llama en cada valor se le pasa un segundo parámetro. Este segundo parámetro es la matriz devuelta por Object.valuesla llamada en el objeto hash principal, o la propia matriz principal.

var getValues = Object.values; // localize
var type_toString = Object.prototype.toString;
function forEachNested(objectIn, functionOnEach){
    "use strict";
    functionOnEach( objectIn );
    
    // for iterating arbitrary objects:
    var allLists = [  ];
    if (type_toString.call( objectIn ) === '[object Object]')
        allLists.push( getValues(objectIn) );
    var allListsSize = allLists.length|0; // the length of allLists
    var indexLists = 0;
    
    // for iterating arrays:
    var allArray = [  ];
    if (type_toString.call( objectIn ) === '[object Array]')
        allArray.push( objectIn );
    var allArraySize = allArray.length|0; // the length of allArray
    var indexArray = 0;
    
    do {
        // keep cycling back and forth between objects and arrays
        
        for ( ; indexArray < allArraySize; indexArray=indexArray+1|0) {
            var currentArray = allArray[indexArray];
            var currentLength = currentArray.length;
            for (var curI=0; curI < currentLength; curI=curI+1|0) {
                var arrayItemInner = currentArray[curI];
                if (arrayItemInner === undefined &&
                    !currentArray.hasOwnProperty(arrayItemInner)) {
                    continue; // the value at this position doesn't exist!
                }
                functionOnEach(arrayItemInner, currentArray);
                if (typeof arrayItemInner === 'object') {
                    var typeTag = type_toString.call( arrayItemInner );
                    if (typeTag === '[object Object]') {
                        // Array.prototype.push returns the new length
                        allListsSize=allLists.push( getValues(arrayItemInner) );
                    } else if (typeTag === '[object Array]') {
                        allArraySize=allArray.push( arrayItemInner );
                    }
                }
            }
            allArray[indexArray] = null; // free up memory to reduce overhead
        }
         
        for ( ; indexLists < allListsSize; indexLists=indexLists+1|0) {
            var currentList = allLists[indexLists];
            var currentLength = currentList.length;
            for (var curI=0; curI < currentLength; curI=curI+1|0) {
                var listItemInner = currentList[curI];
                functionOnEach(listItemInner, currentList);
                if (typeof listItemInner === 'object') {
                    var typeTag = type_toString.call( listItemInner );
                    if (typeTag === '[object Object]') {
                        // Array.prototype.push returns the new length
                        allListsSize=allLists.push( getValues(listItemInner) );
                    } else if (typeTag === '[object Array]') {
                        allArraySize=allArray.push( listItemInner );
                    }
                }
            }
            allLists[indexLists] = null; // free up memory to reduce overhead
        }
    } while (indexLists < allListsSize || indexArray < allArraySize);
}

(function(){"use strict";

var cars = {"label":"Autos","subs":[{"label":"SUVs","subs":[]},{"label":"Trucks","subs":[{"label":"2 Wheel Drive","subs":[]},{"label":"4 Wheel Drive","subs":[{"label":"Ford","subs":[]},{"label":"Chevrolet","subs":[]}]}]},{"label":"Sedan","subs":[]}]};

var lookForCar = prompt("enter the name of the car you are looking for (e.g. 'Ford')") || 'Ford';
lookForCar = lookForCar.replace(/[^ \w]/g, ""); // incaseif the user put quotes or something around their input
lookForCar = lookForCar.toLowerCase();





var getValues = Object.values; // localize
var type_toString = Object.prototype.toString;
function forEachNested(objectIn, functionOnEach){
    functionOnEach( objectIn );
    
    // for iterating arbitrary objects:
    var allLists = [  ];
    if (type_toString.call( objectIn ) === '[object Object]')
        allLists.push( getValues(objectIn) );
    var allListsSize = allLists.length|0; // the length of allLists
    var indexLists = 0;
    
    // for iterating arrays:
    var allArray = [  ];
    if (type_toString.call( objectIn ) === '[object Array]')
        allArray.push( objectIn );
    var allArraySize = allArray.length|0; // the length of allArray
    var indexArray = 0;
    
    do {
        // keep cycling back and forth between objects and arrays
        
        for ( ; indexArray < allArraySize; indexArray=indexArray+1|0) {
            var currentArray = allArray[indexArray];
            var currentLength = currentArray.length;
            for (var curI=0; curI < currentLength; curI=curI+1|0) {
                var arrayItemInner = currentArray[curI];
                if (arrayItemInner === undefined &&
                    !currentArray.hasOwnProperty(arrayItemInner)) {
                    continue; // the value at this position doesn't exist!
                }
                functionOnEach(arrayItemInner, currentArray);
                if (typeof arrayItemInner === 'object') {
                    var typeTag = type_toString.call( arrayItemInner );
                    if (typeTag === '[object Object]') {
                        // Array.prototype.push returns the new length
                        allListsSize=allLists.push( getValues(arrayItemInner) );
                    } else if (typeTag === '[object Array]') {
                        allArraySize=allArray.push( arrayItemInner );
                    }
                }
            }
            allArray[indexArray] = null; // free up memory to reduce overhead
        }
         
        for ( ; indexLists < allListsSize; indexLists=indexLists+1|0) {
            var currentList = allLists[indexLists];
            var currentLength = currentList.length;
            for (var curI=0; curI < currentLength; curI=curI+1|0) {
                var listItemInner = currentList[curI];
                functionOnEach(listItemInner, currentList);
                if (typeof listItemInner === 'object') {
                    var typeTag = type_toString.call( listItemInner );
                    if (typeTag === '[object Object]') {
                        // Array.prototype.push returns the new length
                        allListsSize=allLists.push( getValues(listItemInner) );
                    } else if (typeTag === '[object Array]') {
                        allArraySize=allArray.push( listItemInner );
                    }
                }
            }
            allLists[indexLists] = null; // free up memory to reduce overhead
        }
    } while (indexLists < allListsSize || indexArray < allArraySize);
}




var foundObject = null;
forEachNested(cars, function(currentValue){
    if(currentValue.constructor === Object &&
      currentValue.label.toLowerCase() === lookForCar) {
        foundObject = currentValue;
    }
});
if (foundObject !== null) {
    console.log("Found the object: " + JSON.stringify(foundObject, null, "\t"));
} else {
    console.log('Nothing found with a label of "' + lookForCar + '" :(');
}

})();
Expandir fragmento

Si tiene un problema con las referencias circulares (por ejemplo, si los valores del objeto A son el propio objeto A, de modo que ese objeto A se contiene a sí mismo), o simplemente necesita las claves, entonces está disponible la siguiente solución más lenta.

function forEachNested(O, f){
    O = Object.entries(O);
    var cur;
    function applyToEach(x){return cur[1][x[0]] === x[1]} 
    while (O.length){
        cur = O.pop();
        f(cur[0], cur[1]);
        if (typeof cur[1] === 'object' && cur[1].constructor === Object && 
          !O.some(applyToEach))
            O.push.apply(O, Object.entries(cur[1]));
    }
}

Debido a que estos métodos no utilizan ningún tipo de recursividad, estas funciones son muy adecuadas para áreas donde puede tener miles de niveles de profundidad. El límite de pila varía mucho de un navegador a otro , por lo que la recursividad a una profundidad desconocida no es muy inteligente en Javascript.

Jack G avatar Sep 30 '2017 23:09 Jack G