20
\$\begingroup\$

Use Case

_.pick creates a shallow clone of an object given a predicate that identifies which keys to keep. pickDeep would perform a deep clone of the object and would "pick" up all nested objects containing the given keys. All containers containing the nested objects would remain and would not be removed.

Requirements

  • Recursively apply a pick to each level in an object.
  • If a property is an object/array, then apply a pick.
  • Keep all object/array only if the descendant properties fulfill the pick.

Improvements

I'd like any constructive criticism to improve this code, but I'm mainly interested in advice pertaining to:

  • Readability
  • Flexibility
  • Performance
  • Implementing this with _.pick and _.cloneDeep

Function

(jsfiddle)

function pickDeep(collection, identity, thisArg) {
    var picked = _.pick(collection, identity, thisArg);
    var collections = _.pick(collection, _.isObject, thisArg);

    _.each(collections, function(item, key, collection) {
        var object;
        if (_.isArray(item)) {
            object = _.reduce(item, function(result, value) {
                var picked = pickDeep(value, identity, thisArg);
                if (!_.isEmpty(picked)) {
                    result.push(picked);
                }
                return result;
            }, []);
        } else {
            object = pickDeep(item, identity, thisArg);
        }

        if (!_.isEmpty(object)) {
            picked[key] = object;
        }

    });

    return picked;
}

Test Data

var data = {
    a: 5,
    b: 6,
    c: 7,
    d: {
        a: 65,
        z: 6,
        d: {
            a: 65,
            k: 5
        }
    },
    e: [
        {a : 5},
        {b : 6},
        {c : 7}
    ],
    f: [
        { 
            b : [  { a: 5, z: 5 } ],
            c : 6
        },
        {
            g: 0   
        }
    ]
};

Test Cases

var testCase1 = { a: 5, b: 6};
pickDeep(testCase1, 'a'); // {a : 5}
var testCase2 = { a: 5, b: { a: 1}, c: { b: 3}};
pickDeep(testCase2, 'a'); // {a: 5, b: {a: 1}}
var testCase3 = { a: 5, b: [ { a: 1}, { c: 2}, {d: 3} ], c: [ {c: 2}, {d : 3}]}
pickDeep(testCase3, 'a'); //{ a: 5, b: [ {a: 1} ]}
var testCase4 = [ {a: 5}, {b: 2}, {c: {a :3 }}, {d: [ {a: 4}] }, z: [ { f: 34}] ];
pickDeep(testCase4, 'a'); // [ {a:5}, {c: {a:3}}, {d: [ {a:4}]}];

Execution Code

function isIn(collection) {
    return function(value, key) {
        return _.contains(collection, key);
    }
}

console.log(pickDeep(data, isIn(['a', 'c'])));
\$\endgroup\$
  • \$\begingroup\$ And what is your question? I haven't tested, but I think it does, what it should. On first sight, I find nothing to complain about. So I am fine with it :] \$\endgroup\$ – Thomas Junk Jul 25 '14 at 18:25
  • \$\begingroup\$ @ThomasJunk I think I've found what was wrong with this code. I'll have to update this post, but what was bothering me was that a lot of the code re-implements the logic that's already available in lodash/underscore. I think the better solution is to wrap the _.pick in logic that allows for a deep traverse, rather than re-implementing the pick functionality. \$\endgroup\$ – Pete Jul 25 '14 at 18:59
  • \$\begingroup\$ Can you provide some more examples, I read your code I'm not sure how you want to handle some cases \$\endgroup\$ – megawac Jul 26 '14 at 16:22
  • \$\begingroup\$ @megawac I've updated the page with the requirements and test cases. I have this nagging feeling that this can easily be done with _.cloneDeep and _.pick together, but I haven't been able to connect the dots. \$\endgroup\$ – Pete Jul 28 '14 at 14:01
  • \$\begingroup\$ pick always returns an object. But your test cases want it to return an array? Your code fails testCase4 due to that consideration \$\endgroup\$ – megawac Jul 28 '14 at 14:35
14
\$\begingroup\$

Here's the most elegant way I can think of writing this. I use transform to handle both Arrays and Objects as your tests showed you wanted to support them, though as pointed out in comments your code didn't.

These changes also allow you to use more than 2 pick properties (pickDeep(set, 'a', 'b', 'c', ['d', 'e'])) as you can with the normal pick/omit.

For lodash 3 and underscore 1.8? you'll need to change _.createCallback to _.iteratee the rest of the code should remain the same.

function pickDeep(collection, predicate, thisArg) {
    if (_.isFunction(predicate)) {
        predicate = _.createCallback(predicate, thisArg);
    } else {
        var keys = _.flatten(_.rest(arguments));
        predicate = function(val, key) {
            return _.contains(keys, key);
        }
    }

    return _.transform(collection, function(memo, val, key) {
        var include = predicate(val, key);
        if (!include && _.isObject(val)) {
            val = pickDeep(val, predicate);
            include = !_.isEmpty(val);
        }
        if (include) {
            _.isArray(collection) ? memo.push(val) : memo[key] = val;
        }
    });
}

This also fixes a the need to check pickDeep({a: [{a: 1}]}) twice to add it to the picked object

\$\endgroup\$
  • \$\begingroup\$ Very nice solution. Just a quick question, what is the difference between transform and reduce? Is the difference that transform will exit early if false is returned? \$\endgroup\$ – Pete Jul 28 '14 at 15:23
  • \$\begingroup\$ Heh I've answered that one before stackoverflow.com/questions/21536627/… \$\endgroup\$ – megawac Jul 28 '14 at 15:23
  • \$\begingroup\$ _.createCallback was deprecated to _.callback. On large collections I get RangeError: Maximum call stack size exceeded \$\endgroup\$ – Jon49 Feb 16 '15 at 21:05
  • \$\begingroup\$ 2 things: Latest lodash v4.6.1, _.rest requires a func so this errors out there. 2nd, is it possible to use this to "omit" certain props based on a criteria? \$\endgroup\$ – Mrchief Mar 23 '16 at 16:44
  • 1
    \$\begingroup\$ To make this work with Lodash v4.17.1: * _.createCallback -> _.iteratee * _.rest -> _.tail * _.contains -> _.includes \$\endgroup\$ – MarcoReni Mar 31 '17 at 16:10
4
\$\begingroup\$

I haven't worked with underscore yet (gasp), BUT, I'd like to give what I can here in case you get no better answer soon!

Readability

Your formatting is nearly flawless. However, Douglas Crockford says:

If a function literal is anonymous, there should be one space between the word function and the ( (left parenthesis). If the space is omited, then it can appear that the function's name is function, which is an incorrect reading.

So if you're alright with some nit-picky feedback, try:

function (item, key, collection)

rather than

function(item, key, collection)

Flexibility

Try breaking down your main function into simpler, sub functions, that each have very specific jobs. This keeps your code decoupled, and extensible. I call this Single Function Function. I learned it here. (Read that, and you instantly evolve as a JavaScript Programmer.)

\$\endgroup\$
  • \$\begingroup\$ I think you're spot on the breaking down the main function. I realized that the fundamental problem I'm trying to solve is to apply a function against a piece of deeply nested data. I think the solution is rather than create a pickDeep function, I should be creating a nest function. Then it would be more generalized and could be used with other methods within underscore or any other library for that matter. \$\endgroup\$ – Pete Jul 28 '14 at 13:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.