roryok
roryok

Reputation: 9645

JavaScript: Adding a nested property to an object that may not exist

Suppose I have a json object in which I record the number of visitors to my site, grouped by browser / version.

let data = {
    browsers: {
        chrome: {
           43 : 13, 
           44 : 11
        }, 
        firefox: {
           27: 9
        }
    }
}

To increment a particular browser, I need to check if several keys exist, and if not, create them.

let uap = UAParser(request.headers['user-agent']);

if (typeof uap.browser !== 'undefined') {

    if (typeof data.browsers === 'undefined') 
        data.browsers = {}

    if (typeof data.browsers[uap.browser.name] === 'undefined')
        data.browsers[uap.browser.name] = {}

    if (typeof data.browsers[uap.browser.name][uap.browser.version] === 'undefined')
        data.browsers[uap.browser.name][uap.browser.version] = 0

    data.browsers[uap.browser.name][uap.browser.version] += 1;
}

The deeper my data structure the crazier things get.

It feels like there must be a neater way to do this in javascript. There's always a neater way. Can anyone enlighten me here?

Upvotes: 4

Views: 3318

Answers (3)

PilotInPyjamas
PilotInPyjamas

Reputation: 987

Here's a very clean and generic solution using Proxy() I have a second solution which is standard ECMAscript 5 if you don't need it so cleanly or need it less browser dependant.

var handler = {
    get: function (target, property) {
        if (property !== "toJSON" && target[property] === undefined) {
            target[property] = new Proxy ({}, handler);
        }
        return target[property];
    }
}
var jsonProxy = new Proxy ({}, handler);

jsonProxy.non.existing.property = 5;
jsonProxy.another.property.that.doesnt.exist = 2;
jsonProxy["you"]["can"]["also"]["use"]["strings"] = 20;

console.log (JSON.stringify (jsonProxy));

You can do the same thing with classes but with a more verbose syntax:

var DynamicJSON = function () {};
DynamicJSON.prototype.get = function (property) {
    if (this[property] === undefined) {
        this[property] = new DynamicJSON ();
    }
    return this[property];
};
DynamicJSON.prototype.set = function (property, value) {
    this[property] = value;
};

var jsonClass = new DynamicJSON ();
jsonClass.get("non").get("existing").set("property", 5);
jsonClass.get("you").get("have").get("to").get("use").set("strings", 20);

console.log (JSON.stringify (jsonClass));

Upvotes: 1

Cerbrus
Cerbrus

Reputation: 72927

This shorter code should do the trick:

if (uap.browser) { // typeof is pretty much redundant for object properties.
    const name = uap.browsers.name; // Variables for readability.
    const version = uap.browser.version;

    // Default value if the property does not exist.
    const browsers = data.browsers = data.browsers || {}; 
    const browser = browsers[name] = browsers[name] || {};
    browser[version] = browser[version] || 0;

    // Finally, increment the value.
    browser[version]++;
}

Note that you were using === where you should've been using = (in === {}).

Let's explain this line:

const browsers = data.browsers = data.browsers || {};

The last part: data.browsers = data.browsers || {} sets data.browsers to be itself if it exists. If it doesn't yet, it's set to be a new empty object.
Then, that whole value gets assigned to browsers, for ease of access.

Now, shorter code shouldn't be top priority, but in cases like this, you can make the code a lot more readable.

Upvotes: 6

Tomer
Tomer

Reputation: 17940

You can give up the if statements and do it like this:

uap.browser = uap.browser || {}

essentially it does the same as the if only much shorter

Upvotes: 2

Related Questions