Thursday, February 13, 2014

Working With JavaScript Object Defaults

The issue is a simple, yet annoying one. You want to access the property of an object, and work with that property value — perhaps calling a method on it, or accessing another property. The issue is that when an object property is undefined, errors happen. For example, if you're expecting a string or an array as a property value, the code would work fine if the property were initialized to an empty string, or an empty array respectively. The trick is, providing sane defaults on objects so that the clients using them don't blow up.

One way to go about about providing defaults is to actually set the property default values when the object is created. This has the disadvantage of having to set, and store, a bunch of values that may never be used by the outside world. Another problem with this approach is that the default value may need to change, dynamically, depending on the state of the object. For example, the default date property value should depend on the current value of the default date format. Further complicating things is the outside environment, in which the object resides — the default values for a property could depend on it.

So, we need flexibility, the ability to generate the the default values for object properties at runtime. There are a couple ways I would go about doing this. The first, and most obvious choice for me would be to use a getter.

function User( name ) {

    this._name = name;

    Object.defineProperty( this, "name", {
        get: function(){
            return typeof this._name === "undefined" ?
                "" : this._name;
        }
    });

}

new User().name;
// → ""
new User( "Jake" ).name;
// → "Jake"

Here, we have a user object that defines a name property. When accessed, this is actually calling and returning the value of the getter function we've defined. The object constructor takes a name argument, which is supposed to be a string, and stores this in the _name property. The underscore denotes a private property, not to be accessed by the outside world. And so if the name argument isn't supplied, the _name property ends up having an undefined value. The code that uses User instances doesn't like undefined. So what our getter function does is it checks for the type of the _name property before returning it. If it's undefined, it returns the sane default — an empty string.

While this approach leads to safer, more elegant code that uses instances of User, it has downsides. Namely, I'm not a fan of private vs public properties in JavaScript. The code to implement the getter is somewhat verbose too, and adds up if there's many of them. On the other hand — it can really pull it's weight if there's a lot of code using instances of User &mdash it no longer has to sanity-check the property value before using it.

Another approach to tackling the object property value problem is use tools like Lodash to provide "just-in-time" defaults. For instance, the subtle but powerful _.defaults() function can solve a similar problem as the code above. The difference being that it's used in the same context as the object instance — it's no intrinsic to the object.

function User( name ) {
    this.name = name;
}

_.defaults( new User(), { name: "" } ).name);
// → ""
_.defaults( new User( "Brian" ), { name: "" } ).name);
// → "Brian"

The code here, and least in the declaration of the User object, is less verbose than the former approach. Instead, it supplies the defaults just as the name property is accessed. Obviously the verbosity would be a different story were there lots of users. However, this approach has the advantage of being lean — I feel confident about patching objects using _defaults(). Either approach works more or less the same, the preferred flavor, as always, depends on the context.

No comments :

Post a Comment