tl;dr

Don't use _.defaults() for options; use _.extend() instead, or at least use _.defaults({}, ...).

The Pattern

If you work in web development you're probably familiar with Underscore or its younger, more hip sibling Lodash. And you might have run across their handy _.defaults() function, which lets you take an object and give it some, yes, default values.

It's useful for the common pattern of passing options to a function, as in this example adapted from the Backbone.js source:

function sync(method, model, options) {
    _.defaults(options || (options = {}), {
        emulateHTTP: Backbone.emulateHTTP,
        emulateJSON: Backbone.emulateJSON
    });
    // ... some code using options
}

This code provides reasonable defaults, while giving you the flexibility to override them and specify other fields besides.

It's also readable and pretty easy to understand, as long as you're OK with the slightly dodgy conditional assignment. What's not to love?

Let's Break It

Take this admittedly contrived example:

function warningMessage(options) {
    _.defaults(options || (options = {}), { prefix: 'WARNING: ' });
    return options.prefix + options.message;
}

function errorMessage(options) {
    _.defaults(options || (options = {}), { prefix: 'ERROR: ' });
    return options.prefix + options.message + '!';
}

var params = { message: 'Hello world' };
console.log(warningMessage(params));
console.log(errorMessage(params));

Can you predict the result? Go ahead and paste the code in the console... I'll still be here when you come back.

Back? Surprised? Not what you intended?

You've probably noticed by now that since _.defaults modifies its first argument, the functions have side effects and are no longer pure.

Why should you care if functions have side effects or modify their arguments? Well, I don't want to get functionally dogmatic on you—personally I'm fairly neutral on functional programming hype. But let me tell you, this kind of sloppiness is what subtle bugs are made of, the kind of subtle bugs that make you want to tear your hair out for days on end if indeed you have any hair left at this point.

What's The Fix Already?

For this application I prefer a different Underscore/Lodash function, _.extend(), which reverses the order of overrides. The modified Backbone.js example looks like this:

function sync(method, model, options) {
    options = _.extend({
        emulateHTTP: Backbone.emulateHTTP,
        emulateJSON: Backbone.emulateJSON
    }, options);
    // ... some code using options
}

As a bonus I think it's more readable than the original.

Keep in mind that _.extend() also modifies its first argument, which in this case is an object literal that's not shared, so we're OK. If you do use the same set of defaults in multiple places you'll want to add an additional empty object as the first argument:

    options = _.extend({}, DEFAULTS, options);

Speaking of readable code, if you work with Backbone.js you should take a look at its annotated source. It's not too long and is worth reading in its entirety, and you'll notice many instances of the patterns in this post.

The same thing works with _.defaults() with the arguments swapped:

    options = _.defaults({}, options, DEFAULTS);

Other Considerations

If you're worrying about performance, don't. Until you get to the point of profiling your application, you're optimizing prematurely.

And that's all. Careful of unintended consequences, folks.

comments powered by