yonatanmn
yonatanmn

Reputation: 1600

Checking if property of property exists

generaly, if I want to check if obj.foo.bar exits, I will do this:

if(
    typeof obj != "undefined" &&
    typeof obj.foo !== "undefined" &&
    typeof obj.foo.bar !== "undefined"
 ){ action() } 

this is quite ugly and repetitious, and sadly I have to use it a lot.

I believe there won't be any function isDefined() available to do so, because even empty function (function isDefined(){}) will throw an error when if(isDefined(obj.foo.bar)){action()}

So, is there any smart way to check that obj.foo.bar exits?

EDIT:

  1. Approaches using string parsing (or just passing variables as string) are extremely problematic - since minifier might be in use.
  2. typeof obj != "undefined" could be replaced with obj. This solution still doesn't satisfy me.

EDIT 2 - Solution:

as proposed here

var a = {
    b: {
        c: 'd'
    }
};

function isDefined(fn) {
    var value;
    try {
        value = fn();
    } catch (e) {
        value = undefined;
    } finally {
        return value !== undefined;
    }
};

// ES5
console.log(
    isDefined(function () { return a.b.c; }), //return true
    isDefined(function () { return a.b.c.d.e.f; }) //returns false
);
// ES6 using the arrow function
console.log(
    isset(() => a.b.c),
    isset(() => a.b.c.d.e.f)
);

I have approved @T.J. Crowder's solution and not @somethinghere's one because at the end the regular solution is shorter, and because try catch statements have other purpose and can cause problems

Upvotes: 2

Views: 559

Answers (3)

somethinghere
somethinghere

Reputation: 17340

You can actually make this a universal function as such:

function isDefined(base){
    // Run through any number of passed arguments, ignoring the first one (`base`)
    for(var i = 1; i < arguments.length; i++){
        // While running, see if they exist and assign it to base, then continue
        base = typeof base[arguments[i]] != "undefined" 
            ? base[arguments[i]] : null;
        // if base is set to false break the loop as theres nothing deeper to find
        if(!base) break;
    }
    // return the value that base was set to
    return base;
}

And then call it like this:

var bar = {foo: {}};
console.log(isDefined(bar, 'foo')); ///= Will be bar[foo] (Object Object)
console.log(isDefined(bar, 'foo', 't')); // Will be 'null'

You will always have to pass one argument to make it work, but if you want a global existence you could pass the window object.

function isDefined(base){
    for(var i = 1; i < arguments.length; i++){
    	base = typeof base[arguments[i]] != "undefined" 
    		? base[arguments[i]] : null;
    	if(!base) break;
    }
    return base;
}


var bar = {foo: {}};
document.write(isDefined(bar, 'foo') + " / " + isDefined(bar, 'foo', 't'));

If you want to see if bar exists first, try using isDefined(window, 'bar', 'foo');

Update

I noticed that values set to false will return false as well, so null is our saviour here.

Update 2

Since you bring up the minifier issue, there is one way that is semi-elegant and allows for single line deployment, but you can't wrap it in a function:

var bar = {};
try { var defined = typeof bar.foo !== "undefined"; } catch(e) { var defined = false; } // defined will be false
var bar = {foo:{}};
try { var defined = typeof bar.foo !== "undefined"; } catch(e) { var defined = false; } // defined will be true

The annoying thing is it needs to be executed before your if, but it does simplify your if quite a bit:

var bar = {};
try { var defined = typeof bar.foo !== "undefined"; } catch(e) { var defined = false; }
if(defined){
    // Code here
}

Hope that helps.

Update 3

There is a way to wrap it in a function after all, hidden quite deep but here is a direct link to the underrated answer: javascript test for existence of nested object key

The key is to wrap the value in a function that returns the value at the time. Its quite clever!

Upvotes: 1

Pierrickouw
Pierrickouw

Reputation: 4704

Something like this should do the trick:

function isDefined(obj, keysString) {
    //get all the keys
    var keys = keysString.split('.');
    var tmp = obj;

    for(var i = 0; i < keys.length; i++) {
        tmp = tmp[keys[i]];

        //slowly construct your 'deep' object while checking if you can       
        if(typeof tmp === 'undefined') {
            return false;
        }

    }

    return true;
}

Usage:

isDefined({test: {nested: true}}, 'test.nested'); //return true
isDefined({test: {nested: true}}, 'test.nested.supernested'); //return false
isDefined({test: {nested: true}}, 'test.othernested.supernested'); //return false

JsFiddle Here

Be careful with minifier that may change the name of your keys.

Upvotes: 0

T.J. Crowder
T.J. Crowder

Reputation: 1074276

You can make that a lot less ugly, because you don't need typeof for most of it:

if (obj && obj.foo && typeof obj.foo.bar !== "undefined") {
    action();
}

If obj.foo.bar cannot be a falsey value (0, "", NaN, null, undefined, or false), you don't need typeof for the last item, either:

if (obj && obj.foo && obj.foo.bar) {
    action();
}

If you really want an isDefined function, you have to get into string parsing as outlined in this other question and its answers, but that can be a problem if you use a minifier that renames (shortens) property names, and does involve string parsing.

Upvotes: 4

Related Questions