Skip to content

Instantly share code, notes, and snippets.

@marvinahv
Created April 10, 2020 02:43
Show Gist options
  • Save marvinahv/fde4796c6dd82e67c8dba82a3347bfb5 to your computer and use it in GitHub Desktop.
Save marvinahv/fde4796c6dd82e67c8dba82a3347bfb5 to your computer and use it in GitHub Desktop.
Tutorial with step-by-step execution for a recursive function that returns all keys of a deeply nested object in javascript
obj = {
  f: 'g'
}
function getAllObjectKeys(obj) {
  let keys = [];
  
  for (let key in obj) {
    // key = f
    
    keys.push(key);
    // keys = ['f']
    
    const val = obj[key]; // 'g'
    const valIsObject = typeof(val) === "object"; // false
    const valIsNotNull = val !== null; // true
    const valIsNotArray = !Array.isArray(val); // true
    
    // Skipped
    if (valIsObject && valIsNotNull && valIsNotArray) {
      const _keys = getAllObjectKeys(val);
      keys = keys.concat(_keys);
    }
  }
  
  // Finished looping through keys, so we return the results
  return keys // ['f']
}

This is our recursive function. It takes an object as an argument and returns all its keys and all the keys of its nested children, regardless of depth.

function getAllObjectKeys(obj) {
  let keys = [];
  
  for (let key in obj) {
    keys.push(key);
    
    const val = obj[key];
    const valIsObject = typeof(val) === "object";
    const valIsNotNull = val !== null;
    const valIsNotArray = !Array.isArray(val);
    
    if (valIsObject && valIsNotNull && valIsNotArray) {
      const _keys = getAllObjectKeys(val);
      keys = keys.concat(_keys);
    }
  }
  
  return keys
}

This is a deeply nested object:

// Main object
const input = {
  a: 'b',
  c: 'd',
  // Level 1 nested object
  e: {
    f: 'g'
  },
  // Level 1 nested object
  h: {
    // Level 2 nested object
    i: {
      j: 'k',
      l: 'm',
      // Level 3 nested object
      n: {
        o: [1,2,3,4]
      }
    }
  },
  p: null
};

Now, we call our function with our nested object and we get all the keys back:

const allObjectKeys = getAllObjectKeys(input);
// ["a", "c", "e", "f", "h", "i", "j", "l", "n", "o", "p"]

This is how it works:

At the beginning of our function, we create an empty array called keys. This will hold all the keys we find.

let keys = [];

At the end of the function, we return this array with all the results.

return keys

In order to get the keys, we simply loop through each one of the keys in the provided object (input) with a for...in loop and add each one of these keys to our results array:

for (let key in obj) {
    keys.push(key);
}

If we run this, we'll get all the keys from the provided object in the first level, and our results would look like this:

keys = ['a', 'c', 'e', 'h', 'p']

However, we are required to return all keys from the nested objects. So the next thing we do is get the value of each of the keys we're iterating over, and check if it is a (nested) object. However, there are some gotchas. In Javascript, both null and Array are also of type "object".

typeof([1,2,3,4]) === "object" // true
typeof(null) === "object" // true

These are also objects in Javascript, but they are not nested objects, meaning they are not key/value pairs. Therefore, they are not the kind of objects we are looking for, since we are only interested in getting keys from nested objects. So, assign these conditions and we check they are all present. With this, we can now get the (level 1) nested objects in our provided object:

const val = obj[key]; // We get the value of the current key in the object
const valIsObject = typeof(val) === "object"; // Check if the value is an object type
const valIsNotNull = val !== null; // Check is not a null object type
const valIsNotArray = !Array.isArray(val); // Check is not an Array object type

// If all of the above conditions are met, then we have a "nested" object as a value in the current key:
if (valIsObject && valIsNotNull && valIsNotArray) {
    // val (the current obj[key]) is a key/value (nested) object
}

Now that we can identify the nested objects in our provided object, we need to do two things: (1) get all the keys from this nested object and (2) check if there is another (level 2) nested object inside this nested object. Sounds familiar? That's exactly what our function is already doing. Since this nested objects could in turn have other nested objects and these others with n levels of depth, this is the perfect case for using recursion.

Recursion is a function that calls itself n number of times. This is what we do in the next line: we call the same function getAllObjectKeys, and provide as an argument the current key value, which is an object. The result of this function, as we know, is an array of keys from the provided object. So we assign the result to a variable and we merge it to our initial results array. Now we have the keys from the initial object plus the keys from the (level 1) nested object.

if (valIsObject && valIsNotNull && valIsNotArray) {
    const _keys = getAllObjectKeys(val);
    keys = keys.concat(_keys);
}

Since this is a recursive function, if the provided (level 1) object also has another nested (level 2) object, then our function will be called again and merge those results back, again and again and again. No matter how deep the object is, we will get all the keys from all these objects. In order to make it more clear, we are going to go through the function step by step, so you can see how it all works together.

obj = {
  i: {
    j: 'k',
    l: 'm',
    n: {
      o: [1,2,3,4]
    }
  }
}
function getAllObjectKeys(obj) {
  let keys = [];
  
  for (let key in obj) {
    // key = i
    
    keys.push(key);
    // Now, keys = ['i']
    
    const val = obj[key]; // { j: 'k', l: 'm', n: { o: [1,2,3,4] } }
    const valIsObject = typeof(val) === "object"; // true
    const valIsNotNull = val !== null; // true
    const valIsNotArray = !Array.isArray(val); // true
    
    // We have a nested object,
    // so call our RECURSIVE function (pause on i)
    // and add the resulting keys to our results.
    if (valIsObject && valIsNotNull && valIsNotArray) {
      const _keys = getAllObjectKeys(val); // ['j', 'l', 'n', 'o']
      keys = keys.concat(_keys);
      // keys = ['i', 'j', 'l', 'n', 'o']
    }
  }
  
  // Finished looping through keys, so we return the results
  return keys // ['i', 'j', 'l', 'n', 'o']
}
obj = {
  j: 'k',
  l: 'm',
  n: {
    o: [1,2,3,4]
  }
}
function getAllObjectKeys(obj) {
  let keys = [];
  
  for (let key in obj) {
    // key = j
    
    keys.push(key);
    // Now, keys = ['j']
    
    const val = obj[key]; // 'k'
    const valIsObject = typeof(val) === "object"; // false
    const valIsNotNull = val !== null; // true
    const valIsNotArray = !Array.isArray(val); // true
    
    // Skipped
    if (valIsObject && valIsNotNull && valIsNotArray) {
      const _keys = getAllObjectKeys(val); //
      keys = keys.concat(_keys);
      //
    }
  }
  // Move to the next key
  
  return keys
}
function getAllObjectKeys(obj) {
  let keys = ['j'];
  
  for (let key in obj) {
    // key = l
    
    keys.push(key);
    // Now, keys = ['j', 'l']
    
    const val = obj[key]; // 'm'
    const valIsObject = typeof(val) === "object"; // false
    const valIsNotNull = val !== null; // true
    const valIsNotArray = !Array.isArray(val); // true
    
    // Skipped
    if (valIsObject && valIsNotNull && valIsNotArray) {
      const _keys = getAllObjectKeys(val); //
      keys = keys.concat(_keys);
      //
    }
  }
  
  // Move to the next key
  
  return keys
}
function getAllObjectKeys(obj) {
  let keys = ['j', 'l'];
  
  for (let key in obj) {
    // key = n
    
    keys.push(key);
    // keys = ['j', 'l', 'n']
    
    const val = obj[key]; // { o: [1,2,3,4] }
    const valIsObject = typeof(val) === "object"; // true
    const valIsNotNull = val !== null; // true
    const valIsNotArray = !Array.isArray(val); // true
    
    // We have a nested object, 
    // so call our RECURSIVE function (pause on n)
    // and add the resulting keys to our results.
    if (valIsObject && valIsNotNull && valIsNotArray) {
      const _keys = getAllObjectKeys(val); // ['o']
      keys = keys.concat(_keys);
      // keys = ['j', 'l', 'n', 'o']
    }
  }
  
  // Finished looping through keys, so we return the results
  return keys // keys = ['j', 'l', 'n', 'o']
}
obj = {
  a: 'b',
  c: 'd',
  e: {
    f: 'g'
  },
  h: {
    i: {
      j: 'k',
      l: 'm',
      n: {
        o: [1,2,3,4]
      }
    }
  },
  p: null
}
function getAllObjectKeys(obj) {
  let keys = [];
  
  for (let key in obj) {
    // key = a
    
    keys.push(key);
    // keys = ['a']
    
    const val = obj[key]; // 'b'
    const valIsObject = typeof(val) === "object"; // false
    const valIsNotNull = val !== null; // true
    const valIsNotArray = !Array.isArray(val); // true
    
    // Skipped
    if (valIsObject && valIsNotNull && valIsNotArray) {
      const _keys = getAllObjectKeys(val);
      keys = keys.concat(_keys);
    }
    
    // Move to the next key
  }
    
  return keys
}
function getAllObjectKeys(obj) {
  let keys = ['a'];
  
  for (let key in obj) {
    // key = c
    
    keys.push(key);
    // keys = ['a', 'c']
    
    const val = obj[key]; // 'd'
    const valIsObject = typeof(val) === "object"; // false
    const valIsNotNull = val !== null; // true
    const valIsNotArray = !Array.isArray(val); // true
    
    // Skipped
    if (valIsObject && valIsNotNull && valIsNotArray) {
      const _keys = getAllObjectKeys(val);
      keys = keys.concat(_keys);
    }
    
    // Move to the next key
  }
    
  return keys
}
function getAllObjectKeys(obj) {
  let keys = ['a', 'c'];
  
  for (let key in obj) {
    // key = e
    
    keys.push(key);
    // keys = ['a', 'c', 'e']
    
    const val = obj[key]; // { f: 'g' }
    const valIsObject = typeof(val) === "object"; // true
    const valIsNotNull = val !== null; // true
    const valIsNotArray = !Array.isArray(val); // true
    
    // We have a nested object, 
    // so call our RECURSIVE function (pause on e)
    // and add the resulting keys to our results.
    if (valIsObject && valIsNotNull && valIsNotArray) {
      const _keys = getAllObjectKeys(val); // ['f']
      keys = keys.concat(_keys);
      // keys = ['a', 'c', 'e', 'f']
    }
      
    // Move to the next key
  }
    
  return keys
}
function getAllObjectKeys(obj) {
  let keys = ['a', 'c', 'e', 'f'];
  
  for (let key in obj) {
    // key = h
    
    keys.push(key);
    // keys = ['a', 'c', 'e', 'f', 'h']
    
    const val = obj[key]; // { i: { j: 'k', l: 'm', n: { o: [1,2,3,4] } } }
    const valIsObject = typeof(val) === "object"; // true
    const valIsNotNull = val !== null; // true
    const valIsNotArray = !Array.isArray(val); // true
    
    // We have another nested object, 
    // so call our RECURSIVE function (pause on h)
    // and add the resulting keys to our results.
    if (valIsObject && valIsNotNull && valIsNotArray) {
      const _keys = getAllObjectKeys(val); // ['i', 'j', 'l', 'n', 'o']
      keys = keys.concat(_keys);
      // keys = ['a', 'c', 'e', 'f', 'h', 'i', 'j', 'l', 'n', 'o']
    }
      
    // Move to the next key
  }
    
  return keys
}
function getAllObjectKeys(obj) {
  let keys = ['a', 'c', 'e', 'f', 'h', 'i', 'j', 'l', 'n', 'o'];
  
  for (let key in obj) {
    // key = p
    
    keys.push(key);
    // keys = ['a', 'c', 'e', 'f', 'h', 'i', 'j', 'l', 'n', 'o', 'p']
    
    const val = obj[key]; // null
    const valIsObject = typeof(val) === "object"; // TRUE
    const valIsNotNull = val !== null; // false
    const valIsNotArray = !Array.isArray(val); // true
    
    // Skipped
    if (valIsObject && valIsNotNull && valIsNotArray) {
      const _keys = getAllObjectKeys(val);
      keys = keys.concat(_keys);
    }
  }
  
  // Finished looping through keys, so we return the results
  return keys // ['a', 'c', 'e', 'f', 'h', 'i', 'j', 'l', 'n', 'o', 'p']
}
obj = {
  o: [1,2,3,4]
}
function getAllObjectKeys(obj) {
  let keys = [];
  
  for (let key in obj) {
    // key = o
    
    keys.push(key);
    // keys = ['o']
    
    const val = obj[key]; // [1,2,3,4]
    const valIsObject = typeof(val) === "object"; // TRUE
    const valIsNotNull = val !== null; // true
    const valIsNotArray = !Array.isArray(val); // false
    
    // Skipped
    if (valIsObject && valIsNotNull && valIsNotArray) {
      const _keys = getAllObjectKeys(val); //
      keys = keys.concat(_keys);
      //
    }
  }
  
  // Finished looping through keys, so we return the results
  return keys // ['o']
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment