46

I have a HUGE collection and I am looking for a property by key someplace inside the collection. What is a reliable way to get a list of references or full paths to all objects containing that key/index? I use jQuery and lodash if it helps and you can forget about infinite pointer recursion, this is a pure JSON response.

fn({ 'a': 1, 'b': 2, 'c': {'d':{'e':7}}}, "d"); 
// [o.c]

fn({ 'a': 1, 'b': 2, 'c': {'d':{'e':7}}}, "e");
// [o.c.d]

fn({ 'aa': 1, 'bb': 2, 'cc': {'d':{'x':9}}, dd:{'d':{'y':9}}}, 'd');
// [o.cc,o.cc.dd]

fwiw lodash has a _.find function that will find nested objects that are two nests deep, but it seems to fail after that. (e.g. http://codepen.io/anon/pen/bnqyh)

Shanimal
  • 11,517
  • 7
  • 63
  • 76

10 Answers10

51

This should do it:

function fn(obj, key) {
    if (_.has(obj, key)) // or just (key in obj)
        return [obj];
    // elegant:
    return _.flatten(_.map(obj, function(v) {
        return typeof v == "object" ? fn(v, key) : [];
    }), true);

    // or efficient:
    var res = [];
    _.forEach(obj, function(v) {
        if (typeof v == "object" && (v = fn(v, key)).length)
            res.push.apply(res, v);
    });
    return res;
}
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 4
    this almost worked for me. I had to change line #3 to be `return [obj[key]];` instead so that I would get an array of the key's values back instead of the wrapping object – Chris Montgomery Jul 31 '14 at 15:40
  • 2
    @ChrisMontgomery: Yeah, but OP wanted "all objects *containing* that key" (probably he's doing a `pluck()` on the result anyway) – Bergi Jul 31 '14 at 16:42
  • 1
    @Bergi makes sense. Your solution works for all my scenarios while the answer from Al Jey had problems with objects inside arrays inside objects. – Chris Montgomery Jul 31 '14 at 17:11
  • 1
    What is the difference here between ```res.push(v)``` and ```res.push.apply(res, v);``` ? – Michahell May 31 '16 at 14:23
  • @MichaelTrouw: [What's the difference between a regular push and an Array.prototype.push.apply?](http://stackoverflow.com/q/35638511/1048572) :-) See also [this answer](http://stackoverflow.com/a/1374131/1048572) and read the [MDN docs on `apply`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply). – Bergi May 31 '16 at 14:29
  • @Bergi nice, I did not know this about ```.push()``` and I missed that ```v``` could be an array (we don't know up front, so this is why ```.apply``` is used) – Michahell May 31 '16 at 17:02
  • 1
    @MichaelTrouw: Actually `v` is always an array if the `if` condition is met, because `fn` always returns an array. We use `push.apply` here to append multiple items at once to the `res` array - like `res = res.concat(v)` would, but without creating a new array. – Bergi May 31 '16 at 17:06
  • @Bergi Ah I yes I missed that. I wanted to ask why ```res.concat(v)``` wasn't used here, but then I thought that maybe ```v``` wasn't always an array. Iff that were true, then ```push.apply``` would have worked for both any object and arrays of objects. That's why I thought this was the case :) Thanks for your explanation! – Michahell May 31 '16 at 23:06
  • @MichaelTrouw: No, now you are confusing the two. `.concat(v)` works for arrays (that are spread) and any other values (that are appended as-is), `push.apply(…, v)` throws an error with non-array-like `v` values. – Bergi May 31 '16 at 23:09
  • @Bergi Wow. And I thought my js knowledge was at least decent. Thanks again for the explanation, I should play with any language first before making any assumptions, duly noted! – Michahell Jun 01 '16 at 08:13
  • With ES6 we now have the spread operator which, if I now understand correctly, could be used as function argument instead of ```f.apply()```: http://es6-features.org/#SpreadOperator – Michahell Jun 01 '16 at 11:49
  • 1
    @MichaelTrouw: Yes, we could do `res.push(...v);` – Bergi Jun 01 '16 at 14:44
  • Can you add a version where it's also possible to specify the wanted key value? like `fn(obj, {name:"bergi"})` – vsync Sep 29 '16 at 15:02
  • @vsync Just replace the `has` check by whatever you want to test. – Bergi Sep 29 '16 at 15:08
  • Doesn't work. Results in error. Would be good if you provided an example of how it is actually called. – mjs Aug 17 '17 at 21:06
  • @momomo Does work, with the examples as given in the question. Would be good if you had posted how you called it and what error you got. – Bergi Aug 17 '17 at 21:22
  • @Bergi Maybe in Node.js or something but I need it for my Browser. Opened console and ran the third example. Immediate error. – mjs Aug 17 '17 at 21:53
  • @momomo You still haven't told me what the error was, but maybe you forgot to load lodash? – Bergi Aug 17 '17 at 22:06
24

a pure JavaScript solution would look like the following:

function findNested(obj, key, memo) {
  var i,
      proto = Object.prototype,
      ts = proto.toString,
      hasOwn = proto.hasOwnProperty.bind(obj);

  if ('[object Array]' !== ts.call(memo)) memo = [];

  for (i in obj) {
    if (hasOwn(i)) {
      if (i === key) {
        memo.push(obj[i]);
      } else if ('[object Array]' === ts.call(obj[i]) || '[object Object]' === ts.call(obj[i])) {
        findNested(obj[i], key, memo);
      }
    }
  }

  return memo;
}

here's how you'd use this function:

findNested({'aa': 1, 'bb': 2, 'cc': {'d':{'x':9}}, dd:{'d':{'y':9}}}, 'd');

and the result would be:

[{x: 9}, {y: 9}]
Eugene Kuzmenko
  • 947
  • 9
  • 11
  • This solution does *not* work with nested arrays, e.g. `findNested({ 'a': [ {'b': {'c': 7}} ]}, 'b')` while @Bergi's does – Chris Montgomery Jul 31 '14 at 17:50
  • 2
    @ChrisMontgomery, that's because the original question only included objects and didn't have any arrays, I've updated the sample. – Eugene Kuzmenko Aug 07 '14 at 19:17
  • @AlJey, for your lodash example I used `var o = {a: {really: 'long'}, obj: {that: {keeps: 'going'}}}` and then `findNested(o, 'that')` which gives me `RangeError: Maximum call stack size exceeded`. First one works splended though! – Jon49 Feb 16 '15 at 20:45
  • Uncaught RangeError: Maximum call stack size exceeded – mjs Aug 16 '17 at 21:59
  • I was using it on window – mjs Aug 16 '17 at 21:59
  • @momomo lol, of course, because it contains circular references, lots of them; this example is only for simple objects – Eugene Kuzmenko Aug 17 '17 at 19:50
  • @EugeneKuzmenko Yes, I know, but it is not of course, i don't have the energy to figure out your code to determine if it handles that case or not. I need to find a property in an Object, that's what I am here for. It could not find it. It failed to do that. A better solution would take care of that ( Just add a property visited on each object ). That way it would work ALWAYS :). – mjs Aug 17 '17 at 21:08
  • @EugeneKuzmenko Thanks, this works and I've added the ability to search for multiple keys. Also returns null instead of blank array. Here's the pieces: ```findNested(obj, keys, memo) { if (keys.includes(i)) { this.findNested(obj[i], keys, memo); return memo.length == 0 ? null : memo;``` Testing: ```this.findNested({thing: 0, list: [{message: 'm'}, {stackTrace: 'st'}], message: 'm2'}, ['message', 'stackTrace'])``` returns ["m","st","m2"] – Mike Katulka Oct 01 '20 at 19:25
5

this will deep search an array of objects (hay) for a value (needle) then return an array with the results...

search = function(hay, needle, accumulator) {
  var accumulator = accumulator || [];
  if (typeof hay == 'object') {
    for (var i in hay) {
      search(hay[i], needle, accumulator) == true ? accumulator.push(hay) : 1;
    }
  }
  return new RegExp(needle).test(hay) || accumulator;
}
Eliran Malka
  • 15,821
  • 6
  • 77
  • 100
Eldad
  • 69
  • 1
  • 1
  • @ryanyuyu - it is readable, not close to minified. it's plain english, but does require quite of bit of IQ points to understand. This is a beautiful yet complex solution which stresses the brain to the limits :) – vsync Sep 29 '16 at 15:04
  • 1
    @vsync the code has since been edited. See the [original revision](https://stackoverflow.com/revisions/28700151/1) – ryanyuyu Sep 29 '16 at 15:22
4

If you can write a recursive function in plain JS (or with combination of lodash) that will be the best one (by performance), but if you want skip recursion from your side and want to go for a simple readable code (which may not be best as per performance) then you can use lodash#cloneDeepWith for any purposes where you have to traverse a object recursively.

let findValuesDeepByKey = (obj, key, res = []) => (
    _.cloneDeepWith(obj, (v,k) => {k==key && res.push(v)}) && res
)

So, the callback you passes as the 2nd argument of _.cloneDeepWith will recursively traverse all the key/value pairs recursively and all you have to do is the operation you want to do with each. the above code is just a example of your case. Here is a working example:

var object = {
    prop1: 'ABC1',
    prop2: 'ABC2',
    prop3: {
        prop4: 'ABC3',
        prop5Arr: [{
                prop5: 'XYZ'
            },
            {
                prop5: 'ABC4'
            },
            {
                prop6: {
                    prop6NestedArr: [{
                            prop1: 'XYZ Nested Arr'
                        },
                        {
                            propFurtherNested: {key100: '100 Value'}
                        }
                    ]
                }
            }
        ]
    }
}
let findValuesDeepByKey = (obj, key, res = []) => (
    _.cloneDeepWith(obj, (v,k) => {k==key && res.push(v)}) && res
)

console.log(findValuesDeepByKey(object, 'prop1'));
console.log(findValuesDeepByKey(object, 'prop5'));
console.log(findValuesDeepByKey(object, 'key100'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.10/lodash.min.js"></script>
Koushik Chatterjee
  • 4,106
  • 3
  • 18
  • 32
3

With Deepdash you can pickDeep and then get paths from it, or indexate (build path->value object)

var obj = { 'aa': 1, 'bb': 2, 'cc': {'d':{'x':9}}, dd:{'d':{'y':9}}}

var cherry = _.pickDeep(obj,"d");

console.log(JSON.stringify(cherry));
// {"cc":{"d":{}},"dd":{"d":{}}}

var paths = _.paths(cherry);

console.log(paths);
// ["cc.d", "dd.d"]

paths = _.paths(cherry,{pathFormat:'array'});

console.log(JSON.stringify(paths));
// [["cc","d"],["dd","d"]]

var index = _.indexate(cherry);

console.log(JSON.stringify(index));
// {"cc.d":{},"dd.d":{}}

Here is a Codepen demo

Yuri Gor
  • 1,353
  • 12
  • 26
  • I got a maximum call stack exceeded error using this trying to search for a property on the window object. – Post Impatica Aug 06 '19 at 20:14
  • Did you turn circular check option on? It's off by default. Share your code and I will help to fix it. – Yuri Gor Aug 06 '19 at 21:02
  • when I try to use `var cherry = _.pickDeep( window, 'Netscape', {checkCircular: true});` I get the error: `TypeError: Right-hand side of 'instanceof' is not an object` – Post Impatica Aug 07 '19 at 00:12
  • I have no Netscape browser for testing, but this works in the chrome: `(function(){ var ld = window._; window._ = null; var cherry = ld.pickDeep( window, 'Netscape', {checkCircular: true}); console.log({cherry}); })(); ` Something weird happen when deepdash tries to iterate over global lodash object it depends on, so move it to some local scope. – Yuri Gor Aug 07 '19 at 12:04
  • I get the exact same error. It also wipes out my local storage for my app every single time too which is weird. I ran this: `(function(){ var ld = _; deepdash(ld); _ = null; var cherry = ld.pickDeep( window, 'Netscape', {checkCircular: true}); console.log({cherry}); })();` – Post Impatica Aug 07 '19 at 12:39
  • Which browser do you use? – Yuri Gor Aug 07 '19 at 12:51
  • Chrome on windows 10. If I inspect window.navigator.appName it shows 'Netscape'. No idea why. I also tried your standalone deepdish script: `var cherry = deepdash.pickDeep( window, 'Netscape', {checkCircular: true}); console.log({cherry});` and got the error: `TypeError: Cannot delete property 'top' of [object Window]` – Post Impatica Aug 07 '19 at 12:56
  • I think this is because the `pickDeep` method tries to clone something native and gets original instead. If you want to find something in global scope - try a bit lower level method `eachDeep` - it's 100% read-only, you can just check the current `key` argument of iteratee: `_.eachDeep(window,(val, key, parent, ctx)=>{ if(key=='Netscape'){ console.log(ctx.path); } },{checkCircular:true});` – Yuri Gor Aug 07 '19 at 13:06
  • also, take a look here https://stackoverflow.com/questions/14573881/why-does-javascript-navigator-appname-return-netscape-for-safari-firefox-and-ch – Yuri Gor Aug 07 '19 at 13:09
2

Something like this would work, converting it to an object and recursing down.

function find(jsonStr, searchkey) {
    var jsObj = JSON.parse(jsonStr);
    var set = [];
    function fn(obj, key, path) {
        for (var prop in obj) {
            if (prop === key) {
                set.push(path + "." + prop);
            }
            if (obj[prop]) {
                fn(obj[prop], key, path + "." + prop);
            }
        }
        return set;
    }
    fn(jsObj, searchkey, "o");
}

Fiddle: jsfiddle

Orwellophile
  • 13,235
  • 3
  • 69
  • 45
Ben McCormick
  • 25,260
  • 12
  • 52
  • 71
1

In case you don't see the updated answer from @eugene, this tweak allows for passing a list of Keys to search for!

// Method that will find any "message" in the Apex errors that come back after insert attempts
// Could be a validation rule, or duplicate record, or pagemessage.. who knows!
// Use in your next error toast from a wire or imperative catch path!   
// message: JSON.stringify(this.findNested(error, ['message', 'stackTrace'])),
// Testing multiple keys: this.findNested({thing: 0, list: [{message: 'm'}, {stackTrace: 'st'}], message: 'm2'}, ['message', 'stackTrace'])
findNested(obj, keys, memo) {
    let i,
        proto = Object.prototype,
        ts = proto.toString,
        hasOwn = proto.hasOwnProperty.bind(obj);
  
    if ('[object Array]' !== ts.call(memo)) memo = [];
  
    for (i in obj) {
      if (hasOwn(i)) {
        if (keys.includes(i)) {
          memo.push(obj[i]);
        } else if ('[object Array]' === ts.call(obj[i]) || '[object Object]' === ts.call(obj[i])) {
          this.findNested(obj[i], keys, memo);
        }
      }
    }
  
    return memo.length == 0 ? null : memo;
}
Mike Katulka
  • 121
  • 5
0

The shortest and simplest solution:

Array.prototype.findpath = function(item,path) {
  return this.find(function(f){return item==eval('f.'+path)});
}
lacmuch
  • 61
  • 3
0

Here's how I did it:

function _find( obj, field, results )
{
    var tokens = field.split( '.' );

    // if this is an array, recursively call for each row in the array
    if( obj instanceof Array )
    {
        obj.forEach( function( row )
        {
            _find( row, field, results );
        } );
    }
    else
    {
        // if obj contains the field
        if( obj[ tokens[ 0 ] ] !== undefined )
        {
            // if we're at the end of the dot path
            if( tokens.length === 1 )
            {
                results.push( obj[ tokens[ 0 ] ] );
            }
            else
            {
                // keep going down the dot path
                _find( obj[ tokens[ 0 ] ], field.substr( field.indexOf( '.' ) + 1 ), results );
            }
        }
    }
}

Testing it with:

var obj = {
    document: {
        payload: {
            items:[
                {field1: 123},
                {field1: 456}
                ]
        }
    }
};
var results = [];

_find(obj.document,'payload.items.field1', results);
console.log(results);

Outputs

[ 123, 456 ]
lewma
  • 181
  • 1
  • 5
0

We use object-scan for data processing tasks. It's pretty awesome once you've wrapped your head around how to use it.

// const objectScan = require('object-scan');

const haystack = { a: { b: { c: 'd' }, e: { f: 'g' } } };
const r = objectScan(['a.*.*'], { joined: true, rtn: 'entry' })(haystack);
console.log(r);
// => [ [ 'a.e.f', 'g' ], [ 'a.b.c', 'd' ] ]
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan@13.8.0"></script>

Disclaimer: I'm the author of object-scan

There are plenty more examples on the website.

vincent
  • 1,953
  • 3
  • 18
  • 24