4

Can you guys help me make a shorthand function determining whether an object property exists? In 99% of the cases I want to use it to check whether the returned json object contains the specified property or not. Note that there is no guarantee that any of the parent properties or even the json object itself must be defined.

I was thinking of something in this manner:

function propertyExists(<property>) {
    // property is something like data.property.property
    return typeof(data) !== "undefined" && typeof(data.property) !== "undefined" && typeof(data.property.property) !== "undefined";
}

I don't know how to write it in a dynamic manner to check all the parent properties. Also the in-parameter should be just a reference to the "data.property.property" and not a string, so I don't know how to find the parent properties within that either.

Lightness Races in Orbit
  • 378,754
  • 76
  • 643
  • 1,055
Yonder
  • 719
  • 2
  • 13
  • 26
  • 1
    An *in* test is likely better than *typeof*. This has been asked a lot lately, someone will probably post a link shortly. – RobG Jul 04 '11 at 12:38

3 Answers3

4

Here's a function I have lying around from a project that tells you if a property is defined (including all of its parent properties, if any):

function isDefined(target, path) {
    if (typeof target != 'object' || target == null) {
        return false;
    }

    var parts = path.split('.');

    while(parts.length) {
        var branch = parts.shift();
        if (!(branch in target)) {
            return false;
        }

        target = target[branch];
    }

    return true;
}

It's supposed to be used like this:

var data = { foo: { bar: 42 } };
isDefined(data, "foo"); // true
isDefined(data, "foo.bar"); // true
isDefined(data, "notfoo"); // false
isDefined(data, "foo.baz"); // false

You can easily tweak this to return the value itself (or null) instead of true/false.

Update: After reading the comments on the question, I googled the Javascript in operator and replaced the typeof test with that. Now the code is written "how it was meant to be".

Jon
  • 428,835
  • 81
  • 738
  • 806
  • I like this one - how can I assume that isDefined("foo.bar") always should be checked on the target "foo" as an object? Should work on isDefined("foo") if foo = {}, and isDefined("foo.a.b") if foo = {a:{b:{}}} – Yonder Jul 04 '11 at 13:06
  • @Yonder: Not sure what you mean. Probably by changing `=== 'undefined'` to `=== 'object'`. – Jon Jul 04 '11 at 13:08
  • I mean changing the signature to function isDefined(path) -> deriving the target from the path ("foo" in "foo.bar"). – Yonder Jul 04 '11 at 13:24
  • i use something similar as well and works great. @Yonder, i think you would pass it in like this: `isDefined(foo, "bar")` where `foo` is your `target` object that you want to begin your check from. look at the example Jon gave - he passes in `data` as the target then checks for `foo.bar`. so effectively, you have "data.foo.bar". – hellatan Jul 04 '11 at 13:27
  • @Yonder: In the general case, it's not possible to do that. How is `function isDefined` supposed to pull e.g. `foo` from its *caller's* scope? The best you can do is lose the parameter and do `var target = window;`, but then you also lose the capability to target objects that do not have global scope. – Jon Jul 04 '11 at 13:38
  • @Jon: Ok, I understand the reason for this signature now. Thanks for the explanation. – Yonder Jul 04 '11 at 13:40
  • Oops: `isDefined({foo:{bar:null}}, 'foo.bar') // TypeError: Result of expression 'target' [null] is not an object.` – RobG Jul 04 '11 at 13:41
  • @RobG: Which browser is giving you that one? – Jon Jul 04 '11 at 13:46
  • You changed the code since that comment from *typeof* to *in*. The new one will fail if *target* is not an object (you can't use *in* on non-objects) so you have to test first if it's an object and not *null*, and perhaps for functions too if they might be in the path (not necessary for JSON but needed for the general case). – RobG Jul 04 '11 at 13:54
  • @RobG: Edited again to incorporate your latest comments (thanks!). However, the function was working for me (in Chrome) just fine with the test case you gave both before and after the edit. – Jon Jul 04 '11 at 13:59
  • Strictly, you should use `target === null` since `undefined == null`, but the typeof test has the same effect so personal preference there. The previous version would have failed if *target* was a primitive, e.g. `var x = 1; alert('foo' in x ); // TypeError: Result of expression 'x' [1] is not a valid argument for 'in'.` Probably one of the few cases where primitives aren't coerced to objects. – RobG Jul 04 '11 at 14:05
  • Modifying the `path` array is less effective than iterating over it. It's a foreign pattern for JavaScript. – katspaugh Jul 06 '11 at 20:12
2

JSON properties are allowed to contain dots as in {"a.b": 42} which makes a dot-string unsuitable for a "deep reference" into an object.

isDefined({"a.b": 42}, 'a.b') // false
isDefined({"a": 42}, 'a.b') // TypeError

So an array may a better choice for a reference or index

function hasProperty(value, index) {
    if (index instanceof Array) {
        return index.length === 0 ||
            (hasProperty(value, index[0])
                && hasProperty(value[index[0]], index.slice(1)));
    }
    return value.hasOwnProperty(index);
}

It is used like this:

hasProperty(42, []); // true
hasProperty(42, ['a']); // false
hasProperty(42, ['a']); // false
hasProperty({a: 42}, 'a'); // true
hasProperty({a: 42}, ['a']); // true
hasProperty({a: 42}, ['a', 'b']); // false
hasProperty({a: {b: 42}}, ['a', 'b']); // true
hasProperty({"a.b": 42}, ['a.b']); // true
hasProperty([1,2,3], 2); // true
hasProperty([1,2,3], 3); // false
hasProperty({a: {b: [1,2,3]}}, ['a', 'b', 2]); // true

Note that hasProperty ignores properties from the prototype due to the use of the prototype function hasOwnProperty.

werk
  • 36
  • 3
1

I can't find other posts at the moment, pretty sure the following is on the right track:

function checkAccess(obj, path) {
  var path = path.split('.');
  var prop;

  for (var i=0, iLen=path.length; i<iLen; i++) {
    if (obj !== null && typeof obj == 'object') {
      prop = path[i];

      if (prop in obj) {
        obj = obj[prop];
      }
    } else {
      return false
    }
  }
  return true;
}

var o = {foo:{bar:null}};

alert(checkAccess(o, 'foo.bar')); // true
alert(checkAccess(o, 'foo.bar.baz')); // false

Note that this should be fine for JSON, but if host objects are involved, all bets are off as typeof is not guaranteed to return object (or anything) in that case. If you need to test host objects, likely try..catch would be the most robust solution unless you are confident that the objects being tested will return expected results.

try {
  alert( o.foo.bar.baz);
} catch(e) {
  alert( 'oops');
}

If you want to use a single parameter, then I'll assume that the base object is a global property:

var checkAccess = (function(global) {
  return function (expr) {
    var path = expr.split('.');
    var obj, prop;

    if (path.length) {
      obj = global[path.shift()];

      for (var i=0, iLen=path.length; i<iLen; i++) {
        if (obj !== null && typeof obj == 'object') {
          prop = path[i];

          if (prop in obj) {
            obj = obj[prop];
          }
        } else {
          return false
        }
      }
      return true;
    }
    return false;
  }
}(this));

Edit

Note that the above just means that attempting to access the path will not return an error, it doesn't mean that it will return a value (it may be undefined, null, or something else).

RobG
  • 142,382
  • 31
  • 172
  • 209