If you’re a Ruby developer working with Rails, at some point you’re going to need to work with JavaScript. While the two languages have many similarities, the fundamental differences in their object models can be quite jarring. CoffeeScript helps to provide a more Ruby-like syntax, but if you’re not careful you can introduce bugs to your code in surprising ways.
Let’s look at an example. Let’s say we have an expensive operation, and need to cache the result. In CoffeeScript, we might write the this code as:
class GiantRobot
smashCache: {}
smashInto: (other) =>
@smashCache[other] ||= @expensiveCalculations(other)
However, this code leads to a surprising problem when we start smashing things together.
ralph = new GiantRobot()
voltron = new GiantRobot()
ralph.smashInto(optimusPrime) # => not cached
ralph.smashInto(optimusPrime) # => cached
voltron.smashInto(optimusPrime) # => cached?!
To see why this occurs, we need to take a look at how objects work in JavaScript. Unlike Ruby, JavaScript has no concept of classes. Instead it constructs its objects using prototypes. If you’re writing your code in CoffeeScript, you can almost always ignore this fact, and write your code as if you were in Ruby or Python. In this case, however, CoffeeScript is making our problem less apparent, so let’s take a look at what this code looks like using plain JavaScript.
function GiantRobot() {}
GiantRobot.prototype.smashCache = {};
GiantRobot.prototype.smashInto = function(other) {
if (!this.smashCache[other]) {
this.smashCache[other] = this.expensiveCalculations(other);
}
return this.smashCache[other];
};
Specifically, our problem stems from the fact that setting smashCache
on the
prototype may not work the way you’d think.
How prototypes work
When we create ralph
with, ralph
technically has no smashCache
property.
When we do this.smashCache
, JavaScript looks for that property on ralph
,
followed by ralph
‘s prototype, then ralph
’s prototype’s prototype, and so forth
until it finds something that has the smashCache
property.
JavaScript provides us a way to see this in action, using the hasOwnProperty
method.
ralph.hasOwnProperty('smashCache') # => false
GiantRobot.prototype.hasOwnProperty('smashCache') # => true
So when we call this.smashCache
, we are always getting back
GiantRobot.prototype.smashCache
, which means when we mutate it, we are
mutating the same instance of smashCache
that is used by every instance of
GiantRobot
. This is only an issue when we’re talking about arrays and objects.
ralph.smashCache = 'some value'
# Variable assignment sets the property directly on the instance
ralph.hasOwnProperty('smashCache') == true
Avoiding mutating the prototype
So how do we get around this? Ironically, the answer is simply to make our original example look more like our Ruby code.
class GiantRobot
constructor: ->
@smashCache = {}
Now each instance will have it’s own separate smashCache, and all is well with the world.
Backbone.js gotcha: Mutating the defaults object
A similar problem exists in Backbone.js when using the defaults object on your models.
class RobotProfile extends Backbone.Model
defaults:
images: []
addImage: (newImage) ->
@get('images').push(newImage) # => Mutates a shared instance
The solution for this problem is even simpler. If we change the defaults property to be a function that returns the same hash, Backbone will call it every time a new instance is created.
class RobotProfile extends Backbone.Model
defaults: ->
images: []
Now our images array will no longer be shared across multiple instances, and we can mutate to our heart’s content.