Get a nested property of a complex object at any level of depth Given a complex object (with nested objects at any depth) I need to be able to quickly retrieve a value for a given property name.

I have designed the following code, I would like to know:


How to tune my code for maximum performance considering passing a big object?
Am I missing some case in data.a.x?
Any other improvements?




var data = {
  a: {
    a1: 'hello',
    a2: true,
    a3: false,
    a4: {},
    a5: [],
    a6: ['red', 'blue', true],
    a7: null,
    a8: undefined,
    a9: NaN,
    a10: function() {},
    a11: Math,
    a12: JSON,
    a13: /a-z/,
    a14: new ReferenceError,
    a15: new Date,
    a16: new Number(7),
    a17: new String("abc"),
    a18: new Boolean(true)
  },
  b: {
    b1: {
      b1a: true,
      b1b: 100
    },
    b2: {
      b2a: true,
      b2b: false,
      b2c: 10.10
    }
  }
}

var app = {
  findProperty: function(data, name) {
    var toType = function(obj) {
      return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase()
    };
    var data = data,
      process = true,
      result = {
        isFound: false,
        value: null
      };
    var recursion = function(data, name) {
      var type = toType(data);
      if (type === 'object') {
        Object.keys(data).forEach(function(property) {
          if (process) {
            if (name in data) {
              process = false;
              result.isFound = true;
              result.value = data[name];
            } else if (typeof data[property] === 'object') {
              recursion(data[property], name);
            }
          }
        });
      }
    };
    recursion(data, name);
    return result;
  }
}

console.clear();
// found
var a1 = app.findProperty(data, 'a1');
console.log('a1', a1.isFound, a1.value);

var a2 = app.findProperty(data, 'a2');
console.log('a2', a2.isFound, a2.value);

var a3 = app.findProperty(data, 'a3');
console.log('a3', a3.isFound, a3.value);

var a4 = app.findProperty(data, 'a4');
console.log('a4', a4.isFound, a4.value);

var a5 = app.findProperty(data, 'a5');
console.log('a5', a5.isFound, a5.value);

var a6 = app.findProperty(data, 'a6');
console.log('a6', a6.isFound, a6.value);

var a7 = app.findProperty(data, 'a7');
console.log('a7', a7.isFound, a7.value);

var a8 = app.findProperty(data, 'a8');
console.log('a8', a8.isFound, a8.value);

var a9 = app.findProperty(data, 'a9');
console.log('a9', a9.isFound, a9.value);

var a10 = app.findProperty(data, 'a10');
console.log('a10', a10.isFound, a10.value);

var a11 = app.findProperty(data, 'a11');
console.log('a11', a11.isFound, a11.value);

var a12 = app.findProperty(data, 'a12');
console.log('a12', a12.isFound, a12.value);

var a13 = app.findProperty(data, 'a13');
console.log('a13', a13.isFound, a13.value);

var a14 = app.findProperty(data, 'a14');
console.log('a14', a14.isFound, a14.value);

var a15 = app.findProperty(data, 'a15');
console.log('a15', a15.isFound, a15.value);

var a16 = app.findProperty(data, 'a16');
console.log('a16', a16.isFound, a16.value);

var a17 = app.findProperty(data, 'a17');
console.log('a17', a17.isFound, a17.value);

var a18 = app.findProperty(data, 'a18');
console.log('a18', a18.isFound, a18.value);


var b = app.findProperty(data, 'b');
console.log('b', b.isFound, b.value);

var b1 = app.findProperty(data, 'b1');
console.log('b1', b1.isFound, b1.value);

var b2 = app.findProperty(data, 'b2');
console.log('b2', b2.isFound, b2.value);

var b1a = app.findProperty(data, 'b1a');
console.log('b1a', b1a.isFound, b1a.value);

var b1b = app.findProperty(data, 'b1b');
console.log('b1b', b1b.isFound, b1b.value);

var b2a = app.findProperty(data, 'b2a');
console.log('b2a', b2a.isFound, b2a.value);

var b2b = app.findProperty(data, 'b2b');
console.log('b2b', b2b.isFound, b2b.value);

// not found
var xxx = app.findProperty(data, 'xxx');
console.log('xxx', xxx.isFound, xxx.value);

var yyy = app.findProperty(data, 'yyy');
console.log('yyy', yyy.isFound, yyy.value);




toTry function reference
You can take advantage of the JSON.stringify's replacer argument.
This way it becomes much more simple:

var app =
  findProperty: function(data, name) {
    var result = {
      isFound: false,
      value: null
    };
    JSON.stringify(data, function(k, v) {
      if (k == name) {
        result.isFound = true;
        result.value = v;
        return undefined; // stop stringification
      } else {
        return v; // continue with same value
      }
    });
    return result;
  }
}


Compared to your original function, there are two points to consider:


one little caveat is that a Date object is returned stringified, e.g. for a15 in your dataexample:


has an original value of new Date.
is returned as Date {Thu Jan 14 2016 18:34:10 GMT+0100} by your function.
is returned as 2016-01-14T17:34:10.220Z by the mine.

your function excludes Array objects, while the mine processes them, e.g. calling app.findProperty(data, '0') will return red (from a6: ['red', 'blue', true] in your dataexample).


My version seems to be faster (approximatively x2) using your data example, but it's not certain how it'll be in any case.

The snippet below tries to illustrate it, simultaneously controlling the results between the two versions:



var data = {
  a: {
    a1: 'hello',
    a2: true,
    a3: false,
    a4: {},
    a5: [],
    a6: ['red', 'blue', true],
    a61: {a61a: 'red', a61b: 'blue'},
    a7: null,
    a8: undefined,
    a9: NaN,
    a10: function() {},
    a11: Math,
    a12: JSON,
    a13: /a-z/,
    a14: new ReferenceError,
    a15: new Date,
    a16: new Number(7),
    a17: new String("abc"),
    a18: new Boolean(true)
  },
  b: {
    b1: {
      b1a: true,
      b1b: 100
    },
    b2: {
      b2a: true,
      b2b: false,
      b2c: 10.10
    }
  }
}

var app = {
  findProperty: function(data, name) {
    var toType = function(obj) {
      return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase()
    };
    var data = data,
      process = true,
      result = {
        isFound: false,
        value: null
      };
    var recursion = function(data, name) {
      var type = toType(data);
      if (type === 'object') {
        Object.keys(data).forEach(function(property) {
          if (process) {
            if (name in data) {
              process = false;
              result.isFound = true;
              result.value = data[name];
            } else if (typeof data[property] === 'object') {
              recursion(data[property], name);
            }
          }
        });
      }
    };
    recursion(data, name);
    return result;
  },

  findProp: function(data, name) {
    var result = {
      isFound: false,
      value: null
    };
    JSON.stringify(data, function(k, v) {
      if (k == name) {
        result.isFound = true;
        result.value = v;
        return undefined; // stop stringification
      } else {
        return v; // continue with same value
      }
    });
    return result;
  }
}

var names = [
  'a', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a61', 'a61a', 'a7', 'a8', 'a9',
  'a10', 'a11', 'a12', 'a13', 'a14', 'a15', 'a16', 'a17', 'a18',
  'b', 'b1', 'b2', 'b1a', 'b1b', 'b2a', 'b2b', 'b2c', 'xxx', 'yyy'
];
var results = {};

function exec(func) {
  results[func] = [];
  begTime = Date.now();
  for (var i = 0, n = names.length; i &lt; n; i++) {
    var name = names[i],
        result = app[func](data, name);
    results[func][i] = result.value;
    console.log(name, result.isFound, result.value);
  }
  console.log(
    '--- using ', func + ':', (Date.now() - begTime) + 'ms');
}

console.clear();
exec('findProperty');
exec('findProp');
for (var i = 0, n = results.findProperty.length; i &lt; n; i++) {
  if (results.findProperty[i] !== results.findProp[i]) {
    console.log(
      '*** Error in ' + i + ':',
      results.findProperty[i], '&lt;--&gt;', results.findProp[i]
    );
  }
}




Apart from the already mentioned Date difference, you'll see an "error" on the NaN value: this is normal since NaN is equal to nothing, even itself.