For this blog post, we?re going to continue our journey through the wonderful world of CoffeeScript, exploring objects and classes.
Also make sure to check out the previous installments:
Objects
Most of us have know how the object literal notation looks like using plain old JavaScript. Here?s a simple example:
var podcast = { title: 'Astronomy Cast', description: 'A fact-based journey through the galaxy.', details: { homepage: 'http://www.astronomycast.com', rss: 'http://www.astronomycast.com/feed/', atom: 'http://www.astronomycast.com/feed/atom/' }, toString: function() { return 'Title: ' + this.title; } };
Lets have a look at how define the exact same object using CoffeeScript.
podcast = title: 'Astronomy Cast' description: 'A fact-based journey through the galaxy.' details: homepage: 'http://www.astronomycast.com' rss: 'http://www.astronomycast.com/feed/' atom: 'http://www.astronomycast.com/feed/atom/' toString: -> return "Title: " + @title
As you can see, the CoffeeScript equivalent involves less overhead which naturally results in a more compact syntax. For object literals we don’t have to provide curly braces and commas, taking full advantage of significant whitespace.
In this basic example, we directly assign values for the different properties which is not very common. Suppose that we want to create podcast objects by calling a factory method specifying the necessary parameter values:
createPodcast = (title, description, homepage, rss, atom) -> title: title description: description details: homepage: homepage rss: rss atom: atom toString: -> return "Title: " + @title title = 'Astronomy Cast' description = 'A fact-based journey through the galaxy.' homepage = 'http://www.astronomycast.com' rss = 'http://www.astronomycast.com/feed/' atom = 'http://www.astronomycast.com/feed/atom/' podcast = createPodcast title, description, homepage, rss, atom console.log podcast console.log podcast.toString()
Again, CoffeeScript can save us some typing here as we don?t need to provide both the property name and the corresponding variable name if they are both the same. In this case we can rely on conventions to create a new podcast object.
createPodcast = (title, description, homepage, rss, atom) -> { title description details: { homepage rss atom } toString: -> return "Title: " + @title }
Note that we need to specify curly braces for both the resulting podcast object as for the nested details object because otherwise we?ll get some parser errors. Either way, CoffeeScript gets the most out of every keystroke :-).
Classes
JavaScript is not a ?classical? language but a prototypal object language. There are several JavaScript libraries out there that have made an attempt to bring some form of class like syntax into the language. It?s my personal belief that by fully embracing the prototypical nature of JavaScript that we can gain the true benefits of this great language. However I do recognize that a class like syntax can save us from the syntactical and mental burden that is often seen with prototypical inheritance in JavaScript. Again, CoffeeScript is here to save the day.
By providing a class-style syntax, CoffeeScript effectively provides us with a useful abstraction on top of JavaScript?s prototype capabilities, also applying all the well-known best practices in the process. This enables us to write code that is much more concise and therefore also more clear while still taking advantage of the benefits of the JavaScript object model.
Let?s talk code, shall we?
class Medium constructor: (@name) -> # Prototype method download: (episode) -> console.log 'Downloading ' + episode + ' of ' + @name # Static method @play: (episode, name) -> console.log 'Playing ' + episode + ' of ' + name playOn: -> console.log 'unknown' class Podcast extends Medium constructor: (name, @description) -> super name listen: -> console.log 'Listening to ' + @name playOn: -> console.log 'iPod' class Screencast extends Medium constructor: (name, @description, @author) -> super name watch: -> console.log 'Watching ' + @name playOn: -> console.log 'iPad'
Here we have three classes. Both the Podcast and the Screencast class derive from the Medium class using the extends keyword. First have a look at the constructor functions. CoffeeScript lets us define dedicated functions that will be executed upon instantiation. Notice the shorthand syntax for property initialization. Instead of constructor: (name) ?> @name = name we can simply use the equivalent constructor: (@name). This shorthand will also work for normal functions outside of classes. Remember, every keystroke counts! The constructor functions of the derived classes invoke the constructor of the base class by simply calling super, which translates into a call to an ancestor method of the same name.
Instance methods and properties can be defined exactly the same way as we saw earlier when we discussed objects. Obviously, these can only be used after we create an instance of a class.
podcast = new Podcast('Astronomy Cast', 'A fact-based journey through the galaxy.') podcast.download 'the first episode' podcast.listen() screencast = new Screencast('RailsCasts', 'Ruby on Rails Screencasts', 'Ryan Bates') screencast.download 'the 267th episode' screencast.watch()
But we can also have static methods and properties. These cannot be invoked on instances of a class but only directly on the class object itself.
# Static method @play: (episode, name) -> console.log 'Playing ' + episode + ' of ' + name ... # Object #<Podcast> has no method play podcast.play 'the first episode' Medium.play('the third episode', 'Hardcore History') Podcast.play('the fourth episode', 'Astronomy Cast')
We?ve already used the shorthand syntax here. We could also use the following notation for defining this static method:
# Static method this.play: (episode, name) -> console.log 'Playing ' + episode + ' of ' + name
Remember from the previous post that CoffeeScript uses the ?@? symbol to alias this. Every keystroke counts!
One of the key tenets of object-oriented programming using a classical language is polymorphism. Don?t worry, CoffeeScript got this covered as well.
play = (medium) -> medium.playOn() play(podcast); # Outputs 'iPod' play(screencast); # Outputs 'iPad'
Here we have a function that takes in a Medium object and simply invokes the playOn() function. The playOn() functions of the derived classes will be invoked and not the one defined in the base class.
In the previous post we mentioned that we can reuse methods from other objects using the call()/apply() methods defined on the prototype of Function. This can also be done when we?re using classes in CoffeeScript.
class PodcastStub constructor: -> @name = 'some funcky stub podcast' podcastStub = new PodcastStub() podcast.listen.apply podcastStub # Outputs 'Listening to some funcky stub podcast'
We can use the hash rocket operator (or fat arrow) to prevent this behavior if we want. By using => instead of ?> we can define a function to the current value of this.
class ContextSafePodcast extends Medium constructor: (name, @description) -> super name listen: => console.log 'Listening to ' + @name playOn: -> console.log 'iPod' contextSafePodcast = new ContextSafePodcast('Astronomy Cast', 'A fact-based journey through the galaxy.') contextSafePodcast.listen.apply podcastStub # Outputs 'Listening to Astronomy Cast'
I?ll round of this post by showing you the equivalent JavaScript code for the small class hierarchy shown in our example.
var Medium, Podcast, Screencast; var __hasProp = Object.prototype.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; }; Medium = (function() { function Medium(name) { this.name = name; } Medium.prototype.download = function(episode) { return console.log('Downloading ' + episode + ' of ' + this.name); }; Medium.play = function(episode, name) { return console.log('Playing ' + episode + ' of ' + name); }; Medium.prototype.playOn = function() { return console.log('unknown'); }; return Medium; })(); Podcast = (function() { __extends(Podcast, Medium); function Podcast(name, description) { this.description = description; Podcast.__super__.constructor.call(this, name); } Podcast.prototype.listen = function() { return console.log('Listening to ' + this.name); }; Podcast.prototype.playOn = function() { return console.log('iPod'); }; return Podcast; })(); Screencast = (function() { __extends(Screencast, Medium); function Screencast(name, description, author) { this.description = description; this.author = author; Screencast.__super__.constructor.call(this, name); } Screencast.prototype.watch = function() { return console.log('Watching ' + this.name); }; Screencast.prototype.playOn = function() { return console.log('iPad'); }; return Screencast; })();
Did I mention that every keystroke counts? 😉
Until next time.
Good series. When I read it on my iPod touch iOS 4.3.1 then the code and text overlap. Both in landscape and portrait. Maybe because of the small screen – maybe something can be adjusted in the template or CSS.
To me,
toString: -> return “Title: ” + this.title
isn’t as expressive as
toString: -> “Title: ” + @title
But both work 🙂
Off course, you’re right. Changed the code samples.