13

I am trying to remove empty objects inside an object, here is an example with the expected output:

var object = {
    a: {
        b: 1,
        c: {
            a: 1,
            d: {},
            e: {
              f: {} 
            }
        }
    },
    b: {}
}


var expectedResult = {
    a: {
        b: 1,
        c: {
            a: 1,
        }
    }
}

I tried using some examples from other StackOverflow questions, however those are just for one level objects.

Georgi Kirilov
  • 569
  • 2
  • 5
  • 12
  • Use a recursive function. If you have a function that receives an object and iterates it looking for empty objects, and removes them, then when it comes to an object that isn't empty, simply call the same function with that inner object as the argument. –  Mar 11 '17 at 14:02
  • @squint please give me an example, i am still starter in javascript, trying to catch up.l – Georgi Kirilov Mar 11 '17 at 14:20

3 Answers3

28

Basic function that removes empty objects

First start with a function that only works with a single level of nesting.

This function removes all properties that reference an empty object:

function clearEmpties(o) {
  for (var k in o) {
    if (!o[k] || typeof o[k] !== "object") {
      continue // If null or not an object, skip to the next iteration
    }

    // The property is an object
    if (Object.keys(o[k]).length === 0) {
      delete o[k]; // The object had no properties, so delete that property
    }
    return o;
  }
}

Handling nested objects using recursion

Now you want to make it recursive so that it will operate on nested objects. So we already have tested if o[k] is an object, and we've tested if there are properties, so if there are, we simply call the function again with that nested object.

function clearEmpties(o) {
  for (var k in o) {
    if (!o[k] || typeof o[k] !== "object") {
      continue // If null or not an object, skip to the next iteration
    }

    // The property is an object
    clearEmpties(o[k]); // <-- Make a recursive call on the nested object
    if (Object.keys(o[k]).length === 0) {
      delete o[k]; // The object had no properties, so delete that property
    }
  }
    return o;
}

So just as the original call to clearEmpties removes properties of the given object that reference an empty object, likewise the recursive call will do the same for the nested objects.


Live demo:

var object = {
  a: {
    b: 1,
    c: {
      a: 1,
      d: {},
      e: { // will need to be removed after f has been removed
         f: {} 
      }
    }
  },

  b: {}
};

clearEmpties(object);
console.log(object);

function clearEmpties(o) {
  for (var k in o) {
    if (!o[k] || typeof o[k] !== "object") {
      continue
    }

    clearEmpties(o[k]);
    if (Object.keys(o[k]).length === 0) {
      delete o[k];
    }
  }
  return o;
}

Short version using Underscore and functional style

function clearEmpties(o) {
  if (_.isFunction(o) || !_.isObject(o)) return o;
  return _.chain(o)
    .mapObject(clearEmpties)
    .pick(p => !(_.isObject(p) && _.isEmpty(p)))
    .value();
}

Short version using lodash and functional style - works with treeshaking

import { isFunction, isObject, isEmpty, isArray, isPlainObject, fromPairs } from "lodash-es";

const removeEmtpyObjects = (o) => {
    if (isFunction(o) || !isPlainObject(o)) return o;

    if (isArray(o)) return o.map(removeEmtpyObjects);

    return fromPairs(
        Object.entries(o)
            .map(([k, v]) => [k, removeEmtpyObjects(v)])
            .filter(([k, v]) => !(v == null || (isObject(v) && isEmpty(v))))
    );
};
Frederik Claus
  • 574
  • 4
  • 15
  • I think you should `return o` no? – Georgi Kirilov Mar 11 '17 at 14:27
  • @GeorgiK.: No need, since all you're wanting to do is mutate the existing object structure with `delete`. However, if you want it to return the original object, you certainly can have it do that. –  Mar 11 '17 at 14:28
  • I've tried it, the objects with {} still remain there – Georgi Kirilov Mar 11 '17 at 14:29
  • @GeorgiK.: Nope, they get removed. I added a runnable demo to the bottom of the answer. –  Mar 11 '17 at 14:32
  • Some improvement added so that nested deletion of properties happens before the `length` check. That way a deletion can cascade upwards in the object hierarchy. – trincot Mar 11 '17 at 14:43
  • @trincot: But doesn't that just cause the recursion to happen on empty objects? It'll work, but there'll be no properties to check. –  Mar 11 '17 at 14:43
  • In my opinion that is better. The end-of-recursion check is normally done at the deep-most level. There is a term for that practice, but it escapes my mind right right now.. – trincot Mar 11 '17 at 14:46
  • @trincot: Oh, I think you're thinking of tail recursion, which requires an escape clause. This is a little different. The `.length === 0` isn't actually an escape. It's just the condition used to determine if the property should be removed. –  Mar 11 '17 at 14:48
  • @GeorgiK.: Then your example isn't an accurate representation of your data. –  Mar 11 '17 at 14:49
  • But in the previous version that condition also served to determine if the recursive call had to be made. I am really regretting I can't think of the term right now, so I could link to a reference to what I mean. – trincot Mar 11 '17 at 14:49
  • @trincot, i dont understand why his worked and yours didnt. – Georgi Kirilov Mar 11 '17 at 14:51
  • @trincot: Yes, but that shouldn't make a difference, since if the condition is `true`, there will be no properties to iterate, so nothing to `delete` or recurse. Don't get me wrong, I'm totally good with your approach too, but given the problem in the question, it should make no difference. –  Mar 11 '17 at 14:51
  • @squint, it made a difference in my situation. – Georgi Kirilov Mar 11 '17 at 14:52
  • @GeorgiK.: The question is why. Does your actual data have non-enumerable properties? –  Mar 11 '17 at 14:53
  • ...actually, non-enumerable properties shouldn't make a difference either, since neither `Object.keys()` nor `for-in` will enumerate them. Only thing left would be inherited properties, which the `for-in` will reach (if enumerable) but `Object.keys()` will not. –  Mar 11 '17 at 14:54
  • @trincot: Oh, you're right. If there's an object that only has empty objects, they need to be removed first. Duh! :D Thanks for the edit! –  Mar 11 '17 at 14:55
  • Well, I extended the example object in the answer that shows the difference: after the removal of `f`, also `e` must be removed. That is tangible difference. But we're mixing two changes here: 1. The fact that the length check is now also made after the recursive call (with potential delete action), and 2. The removal of the condition for executing the recursive call -- which has no effect on the result, but is (I think) better practice, since the `for` loop in the nested call will do this check anyway (implicitly). – trincot Mar 11 '17 at 14:56
  • 1
    Finally found the term I was looking for: ["arm's length recursion"](https://en.wikipedia.org/wiki/Recursion_(computer_science)#Short-circuiting_the_base_case), and an opinionated view on it on [quora.com](https://www.quora.com/What-is-arms-length-recursion). – trincot Mar 11 '17 at 15:04
  • @trincot: Excellent. Thank you! –  Mar 11 '17 at 15:04
  • 2
    Note that this will remove Date properties as well, which too are objects. Use something like `Object.prototype.toString.call(o[k]) != '[object Object]'` – Zoran Bosnjak Mar 29 '19 at 14:30
2

I had this same problem and with the addition that my object may contain arrays with empty elements that need to be removed as well.

I ended up with this quick and dirty solution.

If you want to define what "empty" means to you, I also added a different function. In my case, I also needed empty strings.

  function isEmpty(obj) {
        if (obj === '' || obj === null || JSON.stringify(obj) === '{}' || JSON.stringify(obj) === '[]' || (obj) === undefined || (obj) === {}) {
            return true
        } else {
            return false
        }
    }
    function removeEmpty(o) {
        if (typeof o !== "object") {
            return o;
        }
        let oKeys = Object.keys(o)
        for (let j = 0; j < oKeys.length; j++) {
            let p = oKeys[j]
            switch (typeof (o[p])) {
                case 'object':
                    if (Array.isArray(o[p])) {
                        for (let i = 0; i < o[p].length; i++) {
                            o[p][i] = removeEmpty(o[p][i])
                            if (isEmpty(o[p][i])) {
                                o[p].splice(i, 1)
                                i--
                            }
                        }
                        if (o[p].length === 0) {
                            if (Array.isArray(o)) {
                                o.splice(parseInt(p), 1)
                                j--
                            } else {
                                delete o[p]
                            }
                        }
                    } else {
                        if (isEmpty(o[p])) {
                            delete o[p]
                        } else {
                            o[p] = removeEmpty(o[p])
                            if (isEmpty(o[p])) {
                                delete o[p]
                            }
                        }
                    }
                    break
                default:
                    if (isEmpty(o[p])) {
                        delete o[p]
                    }
                    break
            }
    
        }
        if (Object.keys(o).length === 0) {
            return
        }
        return o
    }

Input:

var a = {
b: 1,
c: {
    d: [1, [[], [], [[[1], []]]], [2, [[], [[]]]], [], [[]]]
},
e: {
    f: [{}, { g: 1 }]
},
h: {
    i: { j: { k: undefined, l: null, m: { n: "", o: 1 } } }
},
p: { q: { r: 1 } }
}
removeEmpty(a)

Output:

   {
    "b": 1,
    "c": {
        "d": [1, [[[[1]]]], [2]]
    },
    "e": {
        "f": [{"g": 1}]
    },
    "h": {
        "i": {
            "j": {
                "m": {
                    "o": 1
                }
            }
        }
    },
    "p": {
        "q": {
            "r": 1
        }
    }
}
Rubén Vega
  • 722
  • 6
  • 11
-2
function clean(obj) {
  for (var propName in obj) { 
    if (obj[propName] === null || obj[propName] === undefined) {
      delete obj[propName];      }
  }
}

EDIT:

function clean(obj) {
      for (var propName in obj) {
        if(typeof obj[propName]=="object")
          clean(obj[propName])
        if (obj[propName] === null || obj[propName] === undefined) 
          delete obj[propName];      
       }
    }