1
\$\begingroup\$

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

\$\endgroup\$
4
  • \$\begingroup\$ Could you provide a more realistic use case? \$\endgroup\$ – 200_success Jan 13 '16 at 15:24
  • \$\begingroup\$ How big is... big? And does it always follow this structure where subsequent keys have the same prefix? \$\endgroup\$ – Sumurai8 Jan 13 '16 at 15:56
  • \$\begingroup\$ @200_success thanks for commenting, regarding the realistic use case.. in my case, it is a complex set of business rules defined in a JS object (crated by parsing a JSON). The script is used extensively in the app. \$\endgroup\$ – GibboK Jan 13 '16 at 19:02
  • \$\begingroup\$ @Sumurai8 no the prefix could be arbitrarily name \$\endgroup\$ – GibboK Jan 13 '16 at 19:04
1
\$\begingroup\$

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 < 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 < n; i++) {
  if (results.findProperty[i] !== results.findProp[i]) {
    console.log(
      '*** Error in ' + i + ':',
      results.findProperty[i], '<-->', 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.

\$\endgroup\$
4
  • \$\begingroup\$ Thanks for your insight I found it very interesting and useful! \$\endgroup\$ – GibboK Jan 14 '16 at 20:18
  • \$\begingroup\$ just a reference note: developer.mozilla.org/en/docs/Web/JavaScript/Reference/… \$\endgroup\$ – GibboK Jan 14 '16 at 20:19
  • 1
    \$\begingroup\$ @GibboK Glad to help. Regarding your second comment, I wonder if you want me to point some peculiar point? (maybe your "just a reference note" means you put it merely in order to further readers have it available, but English is not my mother language so I'm not sure). \$\endgroup\$ – cFreed Jan 14 '16 at 22:14
  • \$\begingroup\$ I think reference to replacer on mozilla it is pretty good so all fine as it is for me :), once again thanks for commenting and your time on this :) \$\endgroup\$ – GibboK Jan 15 '16 at 7:12
1
\$\begingroup\$

There is one opportunity I can see to make it a little more performing than it is currently. Below is my implementation of the app.findProperty function. It is checking for the if name in data outside of the Object.keys(data) for each loop.

The reason being, the if condition if (name in data) is not dependent on the looping variable property in the Object.keys(data) for each loop and hence it would evaluate to be the same no matter how many times it is evaluated. That evaluation with in the Object.keys(data) for each loop would evaluate it multiple times causing the code to run slower. Also if your property "name" is available in the data, and your if (name in data) evaluates to true, there is no need to even enumerate the object's keys at that level.

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') {
        if (name in data) {
            process = false;
            result.isFound = true;
            result.value = data[name];
            return result;
        }
        Object.keys(data).forEach(function(property) {
          if (process) {
            if (typeof data[property] === 'object') {
              recursion(data[property], name);
            }
          }
        });
      }
    };
    recursion(data, name);
    return result;
  }
}

While this is a good academic project, for learning, there are several real world cases that the code doesn't handle like what if the property is occurring multiple times in different objects at different depths. How would you be able to query where a specific property a.b.b1.b1b exists no matter b1b is a property defined at several levels. For a more holistic solution for querying JSON, there is a good npm package called json-query.

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.