12

This is a question similar to How to remove undefined and null values from an object using lodash?. However, the solutions proposed there do not conserve the constructor. In addition to that, I want to only delete those keys which starts, say with '_'.

Here is what I am looking for, and can't seem to manage to get from lodash :

Input : new Cons({key1 : 'value1', key2 : {key21 : 'value21', _key22: undefined}, key3: undefined, _key4 : undefined})

Output : {key1 : 'value1', key2 : {key21 : 'value21'}, key3: undefined}

where for example function Cons(obj){_.extend(this, obj)}

I have a solution with omitBy using lodash, however, I loose the constructor information (i.e. I cannot use instanceof Cons anymore to discriminate the object constructor). forIn looked like a good candidate for the recursive traversal but it only provides me with the value and the key. I also need the path in order to delete the object (with unset).

Please note that:

  • the object is any valid javascript object
  • the constructor is any javascript valid constructor, and the object comes with the constructor already set.
  • the resulting object must have instanceof whatevertheconstructorwas still true

Is there a better solution (with lodash or else)?

Community
  • 1
  • 1
user3743222
  • 18,345
  • 5
  • 69
  • 75

5 Answers5

16

You can create a function that recursively omits keys through the use of omitBy() and mapValues() as an assisting function for traversing keys recursively. Also note that this also supports array traversal for objects with nested arrays or top level arrays with nested objects.

function omitByRecursively(value, iteratee) {
  var cb = v => omitByRecursively(v, iteratee);
  return _.isObject(value)
    ? _.isArray(value)
      ? _.map(value, cb)
      : _(value).omitBy(iteratee).mapValues(cb).value()
    : value;
}

function Cons(obj) { 
  _.extend(this, omitByRecursively(obj, (v, k) => k[0] === '_'));
}

Example:

function omitByRecursively(value, iteratee) {
  var cb = v => omitByRecursively(v, iteratee);
  return _.isObject(value)
    ? _.isArray(value)
      ? _.map(value, cb)
      : _(value).omitBy(iteratee).mapValues(cb).value()
    : value;
}

function Cons(obj) { 
  _.extend(this, omitByRecursively(obj, (v, k) => k[0] === '_'));
}

var result = new Cons({
  key1 : 'value1', 
  key2 : {
    key21 : 'value21', 
    _key22: undefined
  }, 
  key3: undefined,
  _key4 : undefined,
  key5: [
    {
      _key: 'value xx',
      key7: 'value zz',
      _key8: 'value aa'
    }
  ]
});

console.log(result);
.as-console-wrapper{min-height:100%;top:0}
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.12.0/lodash.js"></script>

Update

You can mutate the object itself by creating a function that recursively traverses the object with each() and settles the removal by unset().

function omitByRecursivelyInPlace(value, iteratee) {

  _.each(value, (v, k) => {

    if(iteratee(v, k)) {
      _.unset(value, k); 
    } else if(_.isObject(v)) {
      omitByRecursivelyInPlace(v, iteratee);  
    }

  });

  return value;

}

function Cons(obj){_.extend(this, obj)}

var result = omitByRecursivelyInPlace(instance, (v, k) => k[0] === '_');

function omitByRecursivelyInPlace(value, iteratee) {
  
  _.each(value, (v, k) => {
    
    if(iteratee(v, k)) {
      _.unset(value, k); 
    } else if(_.isObject(v)) {
      omitByRecursivelyInPlace(v, iteratee);  
    }
    
  });
  
  return value;
  
}

function Cons(obj){_.extend(this, obj)}

var instance = new Cons({
  key1 : 'value1', 
  key2 : {
    key21 : 'value21', 
    _key22: undefined
  }, 
  key3: undefined,
  _key4 : undefined,
  key5: [
    {
      _key: 'value xx',
      key7: 'value zz',
      _key8: 'value aa'
    }
  ]
});

var result = omitByRecursivelyInPlace(instance, (v, k) => k[0] === '_');

console.log(result instanceof Cons);
console.log(result);
.as-console-wrapper{min-height:100%;top:0}
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.12.0/lodash.js"></script>
ryeballar
  • 29,658
  • 10
  • 65
  • 74
  • Thanks for the answer but once again, the `Cons` is just an example. The constructor can be anything and the object comes with it. So there is no way to modify the constructor to fit my needs. And as mentioned the difficulty is to modify the object in place so the object still has `instanceof Cons` true. – user3743222 May 16 '16 at 09:57
  • That looks absolutely amazing, that's exactly what I was looking for. Thank you so much. so `each` was the way. – user3743222 May 16 '16 at 12:37
  • This does not handle arrays correctly - The check `_.isObject()` will pass if the value is an array, which will then call `mapValues` on the array. The result is that all arrays in the object get replaced by objects, whose keys are the index of the value in the array. A solution would be to replace `_.isObject` with `typeof value === 'object' && !Array.isArray(value)` – Yevgeny Krasik Oct 16 '18 at 08:27
  • @YevgenyKrasik check out my update, it supports array traversal for objects with nested arrays and vice versa. – ryeballar Oct 19 '18 at 14:52
  • 1
    @ryeballar Still, you get an "empty" index for arrays. i.e.: `[1, undefined, null, 2]` becomes `[1, empty, null, 2]` (in case you're using an iteratee like `v => v === undefined`) – Frondor Dec 04 '20 at 11:52
  • The code above uses omits values recursively in a key-value object. Since you're referring to an array, then perhaps you may want to modify the solution to use a `filter` when it sees an array to satisfy the iteratee conditions. – ryeballar Dec 05 '20 at 05:01
1

You can use the rundef package.

By default, it will replace all top-level properties with values set to undefined. However it supports the following options:

  • mutate - set this to false to return the same object that you have provided; this will ensure the constructor is not changed
  • recursive - set this to true to recursively process your object

Therefore, for your use case, you can run:

rundef(object, false, true)
d4nyll
  • 11,811
  • 6
  • 54
  • 68
  • I don't understand how this solves the OP problem. This package won't remove the keys that start with `_` as the OP asks... – a--m Jun 27 '18 at 06:32
  • @a--m You're right, it doesn't. But the main question is "How to delete recursively undefined properties from an object - while keeping the constructor chain?", and I think the package provides a good alternative for people who land on this question without the `_` requirement. – d4nyll Jun 27 '18 at 09:33
0

You can use JSON.stringify(), JSON.parse(), RegExp.prototype.test()

JSON.parse(JSON.stringify(obj, function(a, b) {
  if (!/^_/.test(a) && b === undefined) {
    return null
  }
  return /^_/.test(a) && !b ? void 0 : b
}).replace(/null/g, '"undefined"'));

var obj = {key1 : 'value1', key2 : {key21 : 'value21', _key22: undefined}, key3: undefined, _key4 : undefined}

var res = JSON.stringify(obj, function(a, b) {
  if (!/^_/.test(a) && b === undefined) {
    return null
  }
  return /^_/.test(a) && !b ? void 0 : b
}).replace(/null/g, '"undefined"');

document.querySelector("pre").textContent = res;

res = JSON.parse(res);

console.log(res)
<pre></pre>
guest271314
  • 1
  • 15
  • 104
  • 177
  • Interesting trick to work on the string equivalent and go back to the object. However JSON.stringify as you know ignores functions, as such the object is not recreated at it should in that case. Also would the resulting object still have `Cons` as a constructor? – user3743222 May 16 '16 at 06:50
  • @user3743222 No functions appear as values at example object at Question? – guest271314 May 16 '16 at 06:52
  • no, you are right. I haven't put an exhaustive list of examples covering all the possible cases. That said it is an object like any other object meaning its properties can be anything. – user3743222 May 16 '16 at 06:53
  • @user3743222 _"However JSON.stringify as you know ignores functions"_ Note, `JSON.stringify()` does iterate function values at replacer function. You could create an object outside of `JSON.stringify()`, set filtered properties, values from within replacer function – guest271314 May 16 '16 at 07:31
  • @naomik _"No point in using JSON.stringify at that point tho"_ ? `JSON.stringify()` is used at `js` at post for recursive iteration of object – guest271314 May 16 '16 at 07:37
0

Disclaimer: I have no idea what lodash supports as built-in functions, but this is very easy to implement with vanilla javascript.

Start with a generic function for filtering your object's keys

// filter obj using function f
// this works just like Array.prototype.filter, except on objects
// f receives (value, key, obj) for each object key
// if f returns true, the key:value appears in the result
// if f returns false, the key:value is skipped
const filterObject = f=> obj=>
  Object.keys(obj).reduce((res,k)=>
    f(obj[k], k, obj) ? Object.assign(res, {[k]: obj[k]}) : res
  , {});

Then a function that filters based on your specific behavior

// filter out keys starting with `_` that have null or undefined values    
const filterBadKeys = filterObject((v,k)=> /^[^_]/.test(k) || v !== null && v !== undefined);

Then call it on an object

filterBadKeys({a: null, _b: null, _c: undefined, z: 1});
//=> { a: null, z: 1 }

This can be easily integrated into your constructor now

function Cons(obj) {
  _.extend(this, filterBadKeys(obj));
  // ...
}

EDIT:

On second thought, instead of butchering a perfectly good function with implicit deep recursion, you could abstract out the generic operations and define a specific "deep" filtering function

const reduceObject = f=> init=> obj=>
  Object.keys(obj).reduce((res,k)=> f(res, obj[k], k, obj), init);

// shallow filter      
const filterObject = f=>
  reduceObject ((res, v, k, obj)=> f(v, k, obj) ? Object.assign(res, {[k]: v}) : res) ({});

// deep filter     
const deepFilterObject = f=>
  reduceObject ((res, v, k, obj)=> {
    if (f(v, k, obj))
        if (v && v.constructor === Object)
            return Object.assign(res, {[k]: deepFilterObject (f) (v)});
        else
            return Object.assign(res, {[k]: v});
    else
        return res;
  }) ({});

const filterBadKeys = deepFilterObject((v,k)=> /^[^_]/.test(k) || v !== null && v !== undefined);

filterBadKeys({a: null, _b: null, _c: undefined, _d: { e: 1, _f: null }, z: 2});
//=> { a: null, _d: { e: 1 }, z: 2 }

Integration into your constructor stays the same

function Cons(obj) {
  _.extend(this, filterBadKeys(obj));
  // ...
}
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Yes, maybe doing manually the traversal is the best way. But with this solution, the traversal is not recursive and the constructor information is lost. For instance, `{a: null, _b: {_d: undefined}, _c: undefined, z: 1}` would not give `{a: null, _b: {}, z: 1}` as expected – user3743222 May 16 '16 at 07:06
  • It sounds like this project is a total mess, but changing this code to a recursive filter is trivial. I'll leave that up to you. Hint: it will go in the `reduce` function. – Mulan May 16 '16 at 07:08
  • The difficulty here is to not alter the constructor information. Lodash's `omitBy` already does the recursive filtering fine. But it copies the object to another one, instead of deleting in place. Deleting in place allows to keep the constructor information intact. Note that the constructor comes as is, and is not chosen by me. Maybe I could just put the constructor back in the object? Like `result.constructor = obj.constructor` or sth like that? – user3743222 May 16 '16 at 07:12
  • @user3743222 I provided a "deep" filter solution – Mulan May 16 '16 at 07:28
  • the recursivity works perfectly fine, but again, I do not have the freedom to call the constructor. The `cons` is an example for testing and specifying. There is no say that the constructor is indeed callable with those arguments. And this must work with ANY object, and ANY constructor. The only way I think is to modify IN PLACE. – user3743222 May 16 '16 at 10:09
0

@ryeballar's answer mostly worked, except I wanted three additional features:

  1. first recurse, then do the iteratee check, since after recursing on the object it is possible that it should be omitted
  2. have it work with arrays
  3. some typing

Also took some ideas from: https://codereview.stackexchange.com/a/58279

export function omitByRecursivelyInPlace<T extends object | null | undefined>(value: T, iteratee: (v: any, k: string) => any) {
    _.each(value, (v, k) => {
        // no longer an if/else
        if (_.isObject(v)) {
            omitByRecursivelyInPlace(v, iteratee);
        }
        if (iteratee(v, k)) {
            // check for array types
            if (_.isArray(value)) _.pullAt(value, [parseInt(k)]);
            else _.unset(value, k);
        }
    });
    return value;
}

Not sure about the performance here. Open to feedback. May not be exactly what the OP is asking for.

See https://github.com/lodash/lodash/issues/723 for discussion around this topic in the official lodash repo. Looks like it won't be supported.

ahong
  • 1,041
  • 2
  • 10
  • 22