Cómo recorrer recursivamente un objeto con JavaScript

Cómo escribir una función que busque un par clave/valor específico en un objeto y llamar a esa función recursivamente para atravesar objetos de una profundidad arbitraria.

Primeros pasos

Para este tutorial, vamos a crear un proyecto de Node.js simple y de un solo archivo. En su computadora, elija una buena ubicación para su archivo (por ejemplo, una carpeta de proyectos) y cree un archivo llamado index.js .

Luego, asegúrese de haber instalado Node.js en su computadora. Mientras que el código que escribimos no depende de Node.js para funcionar, lo necesitaremos para ejecutar o ejecutar el código que escribimos dentro de index.js .

Una vez que haya creado su archivo e instalado Node.js, estamos listos para comenzar.

Crear una función para unir objetos por clave y valor

Una manera fácil de entender el concepto de recursividad es pensar en una escalera de caracol en una casa. Para ir desde la parte superior de la escalera hasta el final, debes bajar un escalón a la vez.

Aunque lo haces automáticamente, técnicamente tienes una "función" en tu cerebro que te dice cómo bajar un escalón a la vez hasta llegar al fondo. Llamas a esa "función" para cada peldaño de la escalera hasta que no haya más peldaños. Mientras camina hacia abajo, le dice a la "función" que se vuelva a llamar a sí misma si hay un paso después del actual.

Así es como funciona la recursividad en JavaScript (o cualquier lenguaje de programación). Escribe una función que realiza una tarea y hace que esa función se vuelva a llamar a sí misma si no ha cumplido con algún requisito, por ejemplo, encontrar un valor anidado o llegar al final de una lista.

Para este tutorial, vamos a escribir una función que se centre en lo primero:encontrar un objeto anidado. Más específicamente, queremos escribir una función recursiva que encuentre un objeto anidado que contenga una clave específica con un valor específico.

Primero, creemos nuestra función base y expliquemos de qué se trata:

/index.js

const findNestedObject = (object = {}, keyToMatch = "", valueToMatch = "") => {
  // We'll implement our function here...
};

Nuestra función tomará tres argumentos:un object para atravesar, un keyToMatch dentro de ese objeto, y un valueToMatch dentro de ese objeto.

/index.js

const isObject = (value) => {
  return !!(value && typeof value === "object" && !Array.isArray(value));
};

const findNestedObject = (object = {}, keyToMatch = "", valueToMatch = "") => {
  if (isObject(object)) {
    // We'll work on finding our nested object here...
  }

  return null;
};

A continuación, para evitar errores de tiempo de ejecución, en el cuerpo de nuestro findNestedObject función, agregamos un if declaración con una llamada a una nueva función que hemos agregado arriba isObject() , pasando el object argumento que se pasó a findNestedObject .

Mirando isObject() , queremos estar seguros de que el objeto que estamos atravesando es realmente un objeto. Para averiguarlo, debemos verificar que el value pasó no es nulo o indefinido, tiene un typeof "objeto", y es no una matriz. Ese último puede parecer extraño. Tenemos que hacer !Array.isArray() porque en JavaScript, Array tiene un typeof "objeto" (lo que significa que nuestro anterior typeof value === "object" la prueba puede ser "engañada" por una matriz que se pasa).

Suponiendo que isObject() devuelve true por el valor que le pasamos, podemos empezar a atravesar el objeto. Si no, como alternativa, desde nuestro findNestedObject() función devolvemos null para indicar que no encontrar una coincidencia.

/index.js

const isObject = (value) => {
  return !!(value && typeof value === "object" && !Array.isArray(value));
};

const findNestedObject = (object = {}, keyToMatch = "", valueToMatch = "") => {
  if (isObject(object)) {
    const entries = Object.entries(object);

    for (let i = 0; i < entries.length; i += 1) {
      const [treeKey, treeValue] = entries[i];

      if (treeKey === keyToMatch && treeValue === valueToMatch) {
        return object;
      }
    }
  }

  return null;
};

Agregando algo de complejidad, ahora queremos comenzar el recorrido de nuestro objeto. Por "atravesar" nos referimos a recorrer cada par clave/valor en el object pasado a findNestedObject() .

Para hacer ese bucle, primero llamamos a Object.entries() pasando nuestro object . Esto nos devolverá una matriz de matrices, donde cada matriz contiene el key del par clave/valor que se está reproduciendo actualmente como el primer elemento y el value del par clave/valor que se está reproduciendo actualmente como el segundo elemento. Así:

const example = {
  first: 'thing',
  second: 'stuff',
  third: 'value',
};

Object.entries(example);

[
  ['first', 'thing'],
  ['second', 'stuff'],
  ['third', 'value']
]

A continuación, con nuestra matriz de pares clave/valor (entradas), agregamos un for bucle para iterar sobre la matriz. Aquí, i será igual al índice del par clave/valor actual que estamos recorriendo. Queremos hacer eso hasta que hayamos repetido todas las totalidades, así que decimos "ejecutar este ciclo mientras i < entries.length y para cada iteración, y 1 al índice actual i ."

Dentro del for bucle, usamos la desestructuración de matriz de JavaScript para acceder a la matriz de par clave/valor actual (indicada por entries[i] ), asignando a cada uno una variable. Aquí, asignamos el primer elemento a la variable objectKey y el segundo elemento a la variable objectValue .

Recuerde:nuestro objetivo es encontrar un objeto por el keyToMatch pasado y valueToMatch . Para encontrar una coincidencia, debemos verificar cada clave y valor en nuestro object para ver si son compatibles. Aquí, asumiendo que encontramos una coincidencia, devolvemos el object ya que cumplía con el requisito de tener el keyToMatch y valueToMatch .

Agregar recursividad para atravesar objetos de una profundidad arbitraria

Ahora viene la parte divertida. En este momento, nuestra función solo puede recorrer un objeto de profundidad de un solo nivel. Esto es genial, pero recuerda, queremos buscar un anidado objeto. Debido a que no sabemos dónde podría estar ese objeto en el "árbol" (un apodo que ocasionalmente escuchará para un objeto de objetos anidados), necesitamos poder "seguir adelante" si uno de los valores en la clave/ pares de valores es en sí mismo un objeto.

Aquí es donde entra en juego nuestra recursividad.

/index.js

const isObject = (value) => {
  return !!(value && typeof value === "object" && !Array.isArray(value));
};

const findNestedObject = (object = {}, keyToMatch = "", valueToMatch = "") => {
  if (isObject(object)) {
    const entries = Object.entries(object);

    for (let i = 0; i < entries.length; i += 1) {
      const [objectKey, objectValue] = entries[i];

      if (objectKey === keyToMatch && objectValue === valueToMatch) {
        return object;
      }

      if (isObject(objectValue)) {
        const child = findNestedObject(objectValue, keyToMatch, valueToMatch);

        if (child !== null) {
          return child;
        }
      }
    }
  }

  return null;
};

Recuerda nuestra analogía de la escalera de antes. En este punto, solo hemos bajado un escalón. Para pasar al siguiente paso, necesitamos decirle a nuestra función que se llame a sí misma nuevamente.

En este caso, sabemos que hay otro "paso" u objeto para atravesar si se pasa objectValue al isObject() la función que configuramos anteriormente devuelve true . Si lo hace , eso significa que debemos verificar si eso el objeto contiene el keyToMatch y valueToMatch estamos buscando.

Para atravesar ese objeto, recursivamente (es decir, para llamar de nuevo a la función en la que nos encontramos actualmente), pasando el objectValue junto con el keyToMatch original y keyToValue (lo que estamos buscando no ha cambiado, solo el objeto que queremos mirar).

Si nuestra llamada recursiva encuentra una coincidencia (es decir, nuestra llamada recursiva a findNestedObject() no devuelve null ), devolvemos ese objeto child . Suponiendo que nuestra llamada recursiva a findNestedObject() no devolvió una coincidencia, nuestro recorrido se detendría. Si nuestro propio hijo tuviera objetos anidados (siguiendo con nuestra analogía, otro "paso" para bajar), nuevamente, llamaríamos findNestedObject() .

Debido a que este código es recursivo, se ejecutará hasta que encuentre un objeto coincidente o agote los objetos anidados disponibles para buscar.

Ahora para una prueba. Intentemos encontrar el objeto en este árbol con un name campo igual a "¡Aquí abajo!"

/index.js

const isObject = (value) => {
  return !!(value && typeof value === "object" && !Array.isArray(value));
};

const findNestedObject = (object = {}, keyToMatch = "", valueToMatch = "") => {
  if (isObject(object)) {
    const entries = Object.entries(object);

    for (let i = 0; i < entries.length; i += 1) {
      const [objectKey, objectValue] = entries[i];

      if (objectKey === keyToMatch && objectValue === valueToMatch) {
        return object;
      }

      if (isObject(objectValue)) {
        const child = findNestedObject(objectValue, keyToMatch, valueToMatch);

        if (child !== null) {
          return child;
        }
      }
    }
  }

  return null;
};

const staircase = {
  step: 5,
  nextStep: {
    step: 4,
    nextStep: {
      step: 3,
      nextStep: {
        step: 2,
        nextStep: {
          name: "Down here!",
          step: 1,
        },
      },
    },
  },
};

const match = findNestedObject(staircase, "name", "Down here!");
console.log(match);
// { name: "Down here!", step: 1 }

const match2 = findNestedObject(staircase, "step", 3);
console.log(match2);
// { step: 3, nextStep: { step: 2, nextStep: { name: "Down here!", step: 1 } } }

Aquí hay una demostración rápida de esto ejecutándose en tiempo real:

Terminando

En este tutorial, aprendimos cómo atravesar recursivamente un objeto usando JavaScript. Aprendimos cómo crear una función base que podía recorrer las claves de un objeto que le pasamos, buscando un par de clave y valor coincidentes. Luego, aprendimos cómo usar esa función recursivamente , llamándolo desde dentro de sí mismo si el valor del par clave/valor que estábamos recorriendo actualmente era un objeto.