For a nested complex object or array, I would like to collect all values for a given property name. Example:
var structure = {
name: 'alpha',
array: [
{ name: 'beta' },
{ name: 'gamma' }
],
object: {
name: 'delta',
array: [
{ name: 'epsilon' }
]
}
};
// expected result: [ 'alpha', 'beta', 'gamma', 'delta', 'epsilon' ]
It's obvious how to achieve this using plain JS, but: Is there any elegant, concise approach using lodash?
[edit] Current variant below. Nicer solutions welcome!
function getPropertyRecursive(obj, property) {
var values = [];
_.each(obj, function(value, key) {
if (key === property) {
values.push(value);
} else if (_.isObject(value)) {
values = values.concat(getPropertyRecursive(value, property));
}
});
return values;
}
For a nested complex object or array, I would like to collect all values for a given property name. Example:
var structure = {
name: 'alpha',
array: [
{ name: 'beta' },
{ name: 'gamma' }
],
object: {
name: 'delta',
array: [
{ name: 'epsilon' }
]
}
};
// expected result: [ 'alpha', 'beta', 'gamma', 'delta', 'epsilon' ]
It's obvious how to achieve this using plain JS, but: Is there any elegant, concise approach using lodash?
[edit] Current variant below. Nicer solutions welcome!
function getPropertyRecursive(obj, property) {
var values = [];
_.each(obj, function(value, key) {
if (key === property) {
values.push(value);
} else if (_.isObject(value)) {
values = values.concat(getPropertyRecursive(value, property));
}
});
return values;
}
Share
Improve this question
edited Oct 2, 2016 at 20:48
qqilihq
asked Oct 2, 2016 at 20:22
qqilihqqqilihq
11.5k9 gold badges56 silver badges95 bronze badges
5 Answers
Reset to default 12This can be done elegantly with the following mixin, which is a recursive version of _.toPairs
:
_.mixin({
toPairsDeep: obj => _.flatMap(
_.toPairs(obj), ([k, v]) =>
_.isObjectLike(v) ? _.toPairsDeep(v) : [[k, v]])
});
then to get the result you want:
result = _(structure)
.toPairsDeep()
.map(1)
.value()
If there are scalar properties other than name
, you'll have to filter them out:
result = _(structure)
.toPairsDeep()
.filter(([k, v]) => k === 'name')
.map(1)
.value()
There's no Lodash/Underscore function that I know if that will do what you're looking for.
So what are you looking to do? Well, specifically you're looking to extract the values of all of the name
properties out of a aggregate structure. How would we generalize that? In other words, if you were looking to add such functionality to Lodash/Underscore, how would you reframe the problem? After all, most people don't want to get the values of the name
properties. You could create a generic function where you supply the name of the property you want, but...thinking even more abstractly than that, what you really want to do is visit all of the nodes in a aggregate structure and do something with them. If we consider aggregate structures in JavaScript as generic trees, we can take a recursive approach using a depth-first walk:
function walk(o, f) {
f(o);
if(typeof o !== 'object') return;
if(Array.isArray(o))
return o.forEach(e => walk(e, f));
for(let prop in o) walk(o[prop], f);
}
Now we can do what you're looking for by walking the structure and adding things to an array:
const arr = [];
walk(structure, x => if(x !== undefined && x.name) arr.push(x.name));
This isn't quite functional enough for my tastes, though...there's a side effect on arr
here. So an even better generic approach (IMO) would be to allow a context object to ride along (or an accumulator if you will, a la Array#reduce
):
function walk(o, f, context) {
f(o, context);
if(typeof o !== 'object') return context;
if(Array.isArray(o)) return o.forEach(e => walk(e, f, context)), context;
for(let prop in o) walk(o[prop], f, context);
return context;
}
Now you can call it like this, side-effect free:
const arr = walk(structure, (x, context) => {
if(x !== undefined && x.name) context.push(x.name);
}, []);
Iterate the object recursively using _.reduce()
:
function getPropertyRecursive(obj, prop) {
return _.reduce(obj, function(result, value, key) {
if (key === prop) {
result.push(value);
} else if (_.isObjectLike(value)) {
return result.concat(getPropertyRecursive(value, prop));
}
return result;
}, []);
}
var structure = {
name: 'alpha',
array: [{
name: 'beta'
}, {
name: 'gamma'
}],
object: {
name: 'delta',
array: [{
name: 'epsilon'
}]
}
};
var result = getPropertyRecursive(structure, 'name');
console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.16.2/lodash.min.js"></script>
You could iterate the object and call it again for arrays or objects. Then get the wanted property.
'use strict';
function getProperty(object, key) {
function iter(a) {
var item = this ? this[a] : a;
if (this && a === key) {
return result.push(item);
}
if (Array.isArray(item)) {
return item.forEach(iter);
}
if (item !== null && typeof item === 'object') {
return Object.keys(item).forEach(iter, item);
}
}
var result = [];
Object.keys(object).forEach(iter, object);
return result;
}
var structure = { name: 'alpha', array: [{ name: 'beta' }, { name: 'gamma' }], object: { name: 'delta', array: [{ name: 'epsilon' }] } };
console.log(getProperty(structure,'name'));
.as-console-wrapper { max-height: 100% !important; top: 0; }
Based on the answer ( https://stackoverflow.com/a/39822193/3443096 ) , here's another idea for mixin:
_.mixin({
extractLeaves: (obj, filter, subnode, subpathKey, rootPath, pathSeparator) => {
var filterKv = _(filter).toPairs().flatMap().value()
var arr = _.isArray(obj) ? obj : [obj]
return _.flatMap(arr, (v, k) => {
if (v[filterKv[0]] === filterKv[1]) {
var vClone = _.clone(v)
delete vClone[subnode]
vClone._absolutePath = rootPath + pathSeparator + vClone[subpathKey]
return vClone
} else {
var newRootPath = rootPath
if (_.isArray(obj)) {
newRootPath = rootPath + pathSeparator + v[subpathKey]
}
return _.extractLeaves(
v[subnode], filter, subnode,
subpathKey, newRootPath, pathSeparator
)
}
})
}
});
This work for this example JSON, where you want to extract leaf-nodes:
{
"name": "raka",
"type": "dir",
"children": [{
"name": "riki",
"type": "dir",
"children": [{
"name": "roko",
"type": "file"
}]
}]
}
Use it this way:
_.extractLeaves(result, {type: "file"}, "children", "name", "/myHome/raka", "/")
And you will get:
[
{
"name": "roko",
"type": "file",
"_absolutePath": "/myHome/raka/riki/roko"
}
]