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
pickto each level in an object. - If a property is an
object/array, then apply apick. - Keep all
object/arrayonly if the descendant properties fulfill thepick.
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
_.pickand_.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'])));
2 Answers 2
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
-
\$\begingroup\$ Very nice solution. Just a quick question, what is the difference between
transformandreduce? Is the difference thattransformwill exit early iffalseis returned? \$\endgroup\$Pete– Pete2014年07月28日 15:23:27 +00:00Commented Jul 28, 2014 at 15:23 -
\$\begingroup\$ Heh I've answered that one before stackoverflow.com/questions/21536627/… \$\endgroup\$megawac– megawac2014年07月28日 15:23:59 +00:00Commented Jul 28, 2014 at 15:23
-
\$\begingroup\$
_.createCallbackwas deprecated to_.callback. On large collections I getRangeError: Maximum call stack size exceeded\$\endgroup\$Jon49– Jon492015年02月16日 21:05:38 +00:00Commented Feb 16, 2015 at 21:05 -
\$\begingroup\$ 2 things: Latest lodash v4.6.1,
_.restrequires a func so this errors out there. 2nd, is it possible to use this to "omit" certain props based on a criteria? \$\endgroup\$Mrchief– Mrchief2016年03月23日 16:44:44 +00:00Commented Mar 23, 2016 at 16:44 -
1\$\begingroup\$ To make this work with Lodash v4.17.1: *
_.createCallback -> _.iteratee*_.rest -> _.tail*_.contains -> _.includes\$\endgroup\$MarcoReni– MarcoReni2017年03月31日 16:10:39 +00:00Commented Mar 31, 2017 at 16:10
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.)
-
\$\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
pickDeepfunction, I should be creating anestfunction. Then it would be more generalized and could be used with other methods withinunderscoreor any other library for that matter. \$\endgroup\$Pete– Pete2014年07月28日 13:54:26 +00:00Commented Jul 28, 2014 at 13:54
You must log in to answer this question.
Explore related questions
See similar questions with these tags.
_.pickin logic that allows for a deep traverse, rather than re-implementing the pick functionality. \$\endgroup\$_.cloneDeepand_.picktogether, but I haven't been able to connect the dots. \$\endgroup\$pickalways returns an object. But your test cases want it to return an array? Your code fails testCase4 due to that consideration \$\endgroup\$