Tuesday, October 8, 2013

CoffeeScript: Create objects referencing other properties

In CoffeeScript, you can easily create an object and set the value of some of its properties:

    section =
        title: 'My section'
        element: $('.my-section')
        width: $('.my-section').width()

The width property is used to cache the width of the body. But the way it is defined isn't ideal as $('body') is duplicated. This can be a concern in terms of performance and DRYness. Instead, you could write:

    body = {}
    body.label = 'Document body'
    body.element = $('body')
    body.width = body.element.width()

I find the lack of indentation makes this code less clear. Also body. needs to be repeated on every line. Using the do keyword, you can introduce an indentation, which makes the code clearer, but this doesn't solve the lack of DRYness:

    body = {}; do ->
        body.label = 'Document body'
        body.element = $('body')
        body.width = body.element.width()

Can JavaScript's new operator solve all our problem? Let's try:

    body = new ->
        @title = 'Document body'
        @element = $('body')
        @width = @element.width()

This look like the perfect solution… unless the last property returns an object, in which case body will be equal to that object. For instance, in the following case, body will point to the document (the body's parent), which, obviously, isn't what we want. This is a result of CoffeeScript functions returning the value of the last expression, and the way new works in JavaScript when new is invoked on a function that returns an object.

    # Doesn't work, as body will point to the document
    body = new ->
        @title = 'Document body'
        @element = $('body')
        @width = @element.width()
        @parent = @element.parent()

Until CoffeeScript adds some syntactic sugar not to return the value of the last expression, you can get around this by explicitly adding a return at the end of the function if the last property is an object or in all cases, if you prefer to avoid a possible mistake:

    body = new ->
        @title = 'Document body'
        @element = $('body')
        @width = @element.width()
        @parent = @element.parent()
        return

With the return, I find this code looses some of its elegance, as its looks more like a sequence of statements than the definition of a data structure. Also, this is error prone, as leaving out the return will work in some cases and not others. Alternatively, you can use an anonymous class, and define the function as its constructor:

    body = new class
        constructor: ->
            @title = 'Document body'
            @element = $('body')
            @width = @element.width()

This makes the code somewhat heavier, harder to understand, especially if you're not declaring many classes in your code. My favorite solution leverages Underscore's _.tap. That function is in general used when chaining operations, but is at its core very simple: it applies the function passed as its second argument to the object passed as first argument, and returns the first argument. Using it we can write:

    body = _.tap {}, (o) ->
        o.title = 'Document body'
        o.element = $('body')
        o.width = o.element.width()

Now, that is a solution I can use.

No comments:

Post a Comment