How can I maintain a variable in a recursive function?

Viewed 528

I'm trying to get all the keys in an object, in a recursive way:

let keys = [];

const getAllKeys = (object) => {
  //Supposedly, `keys` should be here.
  Object.keys(object).some((k) => {
    if (object[k] && typeof object[k] === "object") {
      keys.push(k);
      getAllKeys(object[k]);
    }
  });
  return keys;
};

const obj = {
  key1: {
    key2: {
      key3: {}
    }
  }
};

getAllKeys(obj);

console.log(keys);

And as you can see, it works, but the problem is that keys is outside this function, but I need it to be inside so that I can just import this function and use it. I tried wrapping it in another, inner function, but it doesn't work.

What can I do here?

5 Answers

You need a wrapper function to hold the accumulator:

const getAllKeys = (object) => {
    let keys = [];

    let getAllKeysInner = (o) => {
        Object.keys(o).forEach((k) => {
            keys.push(k);
            if (o[k] && typeof o[k] === "object") {
                getAllKeysInner(o[k]);
            }
        });
    }

    getAllKeysInner(object)
    return keys;
};

//

const obj = {
    key1: {
        key2: {
            key3: {}
        }
    }
};

console.log(getAllKeys(obj))

Note that your code needed a few fixes.

FWIW, this particular problem can be done simpler, without any accumulator at all:

const getAllKeys = (o) =>
    (o && typeof o === "object")
        ? Object.keys(o).flatMap(k => [k, ...getAllKeys(o[k])])
        : []

although the accumulator version might be more efficient since it doesn't allocate 1000s temporary arrays.

Finally, the VLAZ's idea to carry the accumulator around is a sane one, because is it as efficient as the wrapper version. It can be written concisely like this:

const getAllKeys = (o, keys=[]) =>
    o
    && typeof o === "object"
    && Object.keys(o).forEach(k => keys.push(k) && getAllKeys(o[k], keys))
    || keys

Use filter and flatMap to get a functional programming solution:

const getAllKeys = object => 
    Object.keys(object)
          .concat(Object.values(object)
                        .filter(obj => Object(obj) === obj)
                        .flatMap(getAllKeys));

const obj = {
  key1: {
    key2: {
      key3: {}
    }
  }
};

console.log(getAllKeys(obj));

You can convert keys to a parameter you pass recursively:

const getAllKeys = (object, keys = []) => {
  Object.keys(object).some((k) => {
    if (object[k] && typeof object[k] === "object") {
      keys.push(k);
      getAllKeys(object[k], keys);
    }
  });
  return keys;
};

const obj = {
  key1: {
    key2: {
      key3: {}
    }
  }
};

const result = getAllKeys(obj);

console.log(result);

This does mean that the first call might be getAllKeys(obj, ["some" "keys", "already", "present"]) or even something invalid like getAllKeys(obj, null). If you want to make sure that doesn't happen, convert your function to a recursion helper function and you can have a top level function that makes sure to call it correctly:

const getAllKeys = (object) => {
  const helper = (object, keys) => {
//      ^^^^^^ now an internal helper
    Object.keys(object).some((k) => {
      if (object[k] && typeof object[k] === "object") {
        keys.push(k);
        helper(object[k], keys);
//      ^^^^^^ recursively calls itself
      }
    });
    return keys;
  }
  
  //top level function just calls the helper:
  return helper(object, []);
};

const obj = {
  key1: {
    key2: {
      key3: {}
    }
  }
};

const result = getAllKeys(obj);

console.log(result);

This would do it:

const getAllKeys=(o,keys=[])=>{
  const gak=object=>{
    Object.keys(object).some((k) => {
      if (object[k] && typeof object[k] === "object") {
        keys.push(k);
        gak(object[k]);
      }
    });
  };
  gak(o);
  return keys;
}

const obj = {
  key1: {
    key2: {
      key3: {}
    }
  }
};

console.log(getAllKeys(obj));

Having keys as an optional argument allows you to re-use it in case you want to collect keys over several objects.

Your function always return an array of keys. You can get result of recursive function inside this function and push it with destructuring syntax:

let nestedKeys = getAllKeys(object[k]);
keys.push(...nestedKeys);

All function could be like this:

const getAllKeys = (object) => {
    let keys = [];

    Object.keys(object).some((k) => {
        if (object[k] && typeof object[k] === "object") {
            keys.push(k);
            let nestedKeys = getAllKeys(object[k]);
            keys.push(...nestedKeys);
        }
    });
    return keys;
};

Also be careful, because code like this typeof object[k] === "object" returns true for Array and null

Related