最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Recursively collect values for property using lodash - Stack Overflow

programmeradmin4浏览0评论

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
Add a comment  | 

5 Answers 5

Reset to default 12

This 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"
  }
]
发布评论

评论列表(0)

  1. 暂无评论