CoffeeScript has classes, but since CoffeeScript is just JavaScript, where do those classes come from? In this article we break down the JavaScript code which is output from a CoffeeScript class and its subclass to see exactly how the magic happens.
Warning: JavaScript Ahead
This article involves some pretty advanced JavaScript. We won’t have time to explain every construct in detail. It also assumes that you’ve read my previous article on prototypes, and that you understand CoffeeScript classes. Of course, you could stop reading right now and continue to write code in ignorance, just like you can eat without knowing much about your stomach. But really, you should stay and learn about the messy guts of what you’re using.
Declassing
Take the following CoffeeScript:
class Bourgeoisie
constructor: (@age, @privilegeConstant) ->
The previous code, translates to this JavaScript:
var Bourgeoisie;
Bourgeoisie = (function() {
function Bourgeoisie(age, privilegeConstant) {
this.age = age;
this.privilegeConstant = privilegeConstant;
}
return Bourgeoisie;
})();
The outermost variable Bourgeoisie
is assigned an IIFE, which is essentially a construct used for controlling scope. The pattern for an IIFE is shown below.
(function(){
//lots of code
return result
})();
Only the things that are returned ever make it to the outside world. In this case, it’s an inner Bourgeoisie
constructor function that is returned. The constructor function attaches properties to the instance being constructed. When it’s returned, the constructor is assigned to the outside Bourgeoisie
variable. Next, we add the following functions.
class Bourgeoisie
constructor: (@age, @privilegeConstant) ->
worry: ->
console.log("My stocks are down 1%!")
profit: (hardWork, luck) ->
return (@age - 23) * hardWork * (luck + @privilegeConstant)
This translates into the following JavaScript.
var Bourgeoisie;
Bourgeoisie = (function() {
function Bourgeoisie(age, privilegeConstant) {
this.age = age;
this.privilegeConstant = privilegeConstant;
}
Bourgeoisie.prototype.worry = function() {
return console.log("My stocks are down 1%!");
};
Bourgeoisie.prototype.profit = function(hardWork, luck) {
return (this.age - 23) * hardWork * (luck + this.privilegeConstant);
};
return Bourgeoisie;
})();
Notice that we’re using the prototype
property of the constructor to add more functions. Doing so places the function into the __proto__
property of each instance, so that it can be used at will. Thus, when we create a new instance of Bourgeoisie
, the age
and privilegeConstant
variables are placed on the instance, while the worry()
and profit()
functions are placed on the prototype of the instance. Using this example as a parent class, let’s explore inheritance.
Inheritance
Take the following Senator
class, which inherits from Bourgeoisie
. Note, the code for Bourgeoisie
is not included, because it has not changed.
class Senator extends Bourgeoisie
worry: ->
console.log("My polls are down 1%!")
Now, let’s see what this simple class looks like in JavaScript.
var Senator,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) {
for (var key in parent) {
if (__hasProp.call(parent, key)) {
child[key] = parent[key];
}
}
function ctor() { this.constructor = child; }
ctor.prototype = parent.prototype;
child.prototype = new ctor();
child.__super__ = parent.prototype;
return child;
};
Senator = (function(_super) {
__extends(Senator, _super);
function Senator() {
return Senator.__super__.constructor.apply(this, arguments);
}
Senator.prototype.worry = function() {
return console.log("My polls are down 1%!");
};
return Senator;
})(Bourgeoisie);
Holy cow. Let’s take this one step at a time. The following code declares the Senator
variable, and creates a shortcut to the hasOwnProperty()
method.
var Senator,
__hasProp = {}.hasOwnProperty,
This next piece of code starts the __extends()
function. The first part manually copies each property of the parent and places it onto the child. Remember that pointers to functions are just variables, so functions are transferred this way as well.
__extends = function(child, parent) {
for (var key in parent) {
if (__hasProp.call(parent, key)) {
child[key] = parent[key];
}
}
...
This next piece is more difficult to parse. First, we create a function called ctor()
which contains, at first, only a constructor function. Then, we assign the prototype
of that constructor function to the parent
, and the prototype
of the child to a new instance of the constructor.
...
function ctor() { this.constructor = child; }
ctor.prototype = parent.prototype;
child.prototype = new ctor();
...
Whew! What does that get us? Well, the prototype of the constructor acts as the parent class, which means the instance will have a __proto__
property containing all of the properties of the parent class. This isn’t too complex, if you followed the discussion in my first explanation of prototypes. The confusing part is the seemingly infinite regress of prototype and constructor.
You see, ctor()
has a constructor property of child
, which has a new instance of ctor()
as its prototype. This gives us child.prototype.constructor = child
. If you examine this in Chrome Dev Tools, you’ll get an infinite regress. Luckily, this doesn’t seem to effect performance, but it is still a confusing bit of architecture.
Thankfully, the last piece (shown below) is much simpler. The child
is given an attribute of __super__
, which is assigned the parent’s prototype
. This is something which our implementation of prototypical inheritance does not easily replicate, and it will be very useful when you want to define a new function on a child but still reference the parent’s version of the function. We will see this used in the code for the Senator
.
...
child.__super__ = parent.prototype;
return child;
};
Finally, we return the child
. To be clear, this is the class definition (or the prototype) for the child
, not a specific instance. The code we just discussed is created once, and then used for every inheritance.
The Senator’s Inheritance
The following section of code is specific to the Senator
‘s inheritance. Notice that the IIFE structure has been modified to take in an argument. The passed in argument is Bourgeoisie
, which is referred to as _super
within the IIFE. Also, the Senator
that is returned is assigned to the Senator
on the outside of the IIFE.
Senator = (function(_super) {
__extends(Senator, _super);
function Senator() {
return Senator.__super__.constructor.apply(this, arguments);
}
Senator.prototype.worry = function() {
return console.log("My polls are down 1%!");
};
return Senator;
})(Bourgeoisie);
The first thing we do within the code block is call __extends()
, which takes Senator
(the child) and _super
(the parent) as arguments. The worry()
function is defined here in the usual way, overwriting the parent’s version. The profit()
function is on Bourgeoisie
, and is thus inherited through __proto__
. More interesting is the constructor function, which we’ll cover now.
Constructing New Instances
The constructor for Senator
is shown below.
function Senator() {
return Senator.__super__.constructor.apply(this, arguments);
}
To make this easier to understand, consider the following functionally equivalent statement. This code is simply calling the constructor function on the parent prototype using the passed in arguments. The first definition, created by CoffeeScript, does the same thing, but with a generalized number of arguments.
function Senator(age, privilegeConstant){
return Senator.__super__.constructor(age, privilegeConstant);
}
The arguments
variable in JavaScript places all of the arguments passed to a function in an array like object, even if they are not explicitly named in the function definition. The other JavaScript trick we use is the apply()
function. apply()
allows you to specify a function’s arguments, as well as the value of this
. In summary, we’re taking an arbitrary number of arguments, and passing them all to the constructor function of the parent’s prototype. In order to pass an arbitrary number of arguments, we use the apply()
function.
Conclusion
We’ve seen how CoffeeScript classes are created and extended by studying the generated JavaScript code. We’ve also covered all of the basic features of classes. Just be aware that the next official version of JavaScript will include its own implementation of classes. They will compile down to prototypes in a manner that is similar (but not identical) to how CoffeeScript classes compile down to prototypes. Stay tuned.