I've been coding javascript for some time now, including interacting with several javascript VMs.
Having said that, this is just a rant about what I think could be a good approach on how to actually do things in javascript. This after looking at how other projects are dealing with it, my personal experience and finally "getting" the way javascript was meant to be used (if there's such a thing). I do not consider myself an expert in javascript, so take whatever you read here with a grain of salt and think of it as just my personal experience and of course, I'll be happy to hear what you know or your best practices around it.
The real purpose for this post is to initiate a discussion about the subject of not really using OOP when dealing with javascript, and the good way to design and think about your code.
I'm a fan of the right tool for the right job. But some things make it too easy to just use a screwdriver to solder a SMD, and javascript is one of that kind of screwdriver. You can rub it hard enough against your board so that it might generate enough heat to melt the solder and let you plug the small SMD at the right time. But you're better off just using the screwdriver to do what it's good at: screwing you.
Why do developers love so much OOP? I think because it's usually what you get taught at School, and you can create quick mental analogies with some black boxes that resemble the encapsulation method provided by the OOP paradigm. That's good, that's actually very good. A great example that can be used for this is Ruby. I just love Ruby, it's so elegant and concise and everything is so object oriented that I feel "safe".
Lately I've been playing and learning Clojure. Clojure is an elegant lisp-like, totally functional language. While reading about it, I remembered that javascript is not actually a real OOP language, but is prototype-based, and it can actually work more as a functional language than a real OOP one, in the sense that you can separate the functionality from the data.
Let me elaborate. Prototype-based inheritance can be better thought in terms of composition. You don't have the concept of rigid structures like classes, interfaces, protocols, etc. You have "the prototype" and that's it. But prototypes are easily modified at runtime, so you can combine them and simulate inheritance and polymorphism by mixing and matching different prototypes.
There's also no real notion of "instantiation" as we know it in OOP, it's just a copy of some other prototype, and some weird constructor function that gets called the first time.
Enough with the talk, now to the code, but first, let's introduce a very simple scenario where we have a single inheritance chain in "regular" javascript.
var ClassA = function (bar) {
this.bar = bar || "XD";
};
ClassA.prototype.foo = function () {
console.log("shazam! " + this.bar);
};
var ClassB = function () {
goog.base(this, "%_%");
};
goog.inherits(ClassB, ClassA);
var ClassC = function () {
goog.base(this);
};
goog.inherits(ClassC, ClassB);
ClassC.prototype.foo = function () {
console.log("not shazam! " + this.bar);
};
var a = new ClassA();
var b = new ClassB();
var c = new ClassC();
a.foo();
b.foo();
c.foo();
That code will output something like the following
shazam! XD
shazam! %_%
not shazam! %_%
Pretty straight forward. By the way, for the inheritance, I'm using closure library's "inherits" from the base module.
goog.inherits = function(childCtor, parentCtor) {
/** @constructor */
function tempCtor() {};
tempCtor.prototype = parentCtor.prototype;
childCtor.superClass_ = parentCtor.prototype;
childCtor.prototype = new tempCtor();
childCtor.prototype.constructor = childCtor;
};
That's one of the most common ways to simulate object orientation in javascript. Works and looks mostly ok and it's pretty straight forward. How about really using prototypes for this.
var ClassA = {
bar: "XD",
foo: function () {
console.log("shazam! " + this.bar);
}
};
var ClassB = Object.create(ClassA, {bar: {value: "%_%"}});
var ClassC = Object.create(ClassB);
ClassC.foo = function () {
console.log("not shazam! " + this.bar);
};
var a = Object.create(ClassA);
var b = Object.create(ClassB);
var c = Object.create(ClassC);
a.foo();
b.foo();
c.foo();
NOTE: I'm using ECMAScript 5's Object.create, but you can use a polyfill if you're targeting older browsers.
Ok... looks a bit more interesting but no real benefit. The problem with the code above is that YOU ARE STILL THINKING WITH IN AN OBJECT ORIENTED WAY. So let's try to re-design it.
var ObjectA = {
bar: "XD"
};
var ObjectB = Object.create(ObjectA, {bar: {value: "%_%"}});
var ObjectC = Object.create(ObjectB);
var foo = function (obj) {
console.log("shazam! " + obj.bar);
};
foo(a);
foo(b);
foo(c);
The first thing you might think by looking at the code above is: hey! it's not really doing what I wanted. And you're right, because the example is stupid and not really a real world example, so let's create a more interesting one. For some reason, OOP introductions love to use animals. I'll use stuffed animals.
var PlushAnimal = {
name: "plushy",
legs: 2,
price: 1e6
}
var PteroBear = Object.create(PlushAnimal);
PteroBear.wings = 2;
var walk = function (animal) {
if (animal.legs) {
console.log(animal.name + " is walking");
}
};
var fly = function (animal) {
if (animal.wings) {
console.log(animal.name + " is flying!");
}
};
var mfb = Object.create(PteroBear, {name: {value: "little bear"}});
var justAnotherToy = Object.create(PlushAnimal);
// perform some actions
walk(mfb);
walk(mfb);
fly(mfb);
walk(justAnotherToy);
fly(justAnotherToy);
Let's recapitulate. What's new here? objects are used only to store information and give context. Functions receive objects operate on them. Simple and clean. If you use closure compiler, you could even remove the if (wings)
, given that you properly type your javascript code (I wouldn't recommend that though).
One benefit of doing it this way, is that you end up with a simpler architecture, based on simple objects that will hold only contextual information: position, name, color, texture, etc. And all the processing is done outside, in functions that are expecting those simple object. What you gain for free is that your objects will be easily serializable (most of the time): regular javascript objects can be serialized with JSON.dump and JSON.parse.
The first time I transitioned from OOP to a more functional/prototype based design, was when creating a Trie. I'll show you first the initial design in the traditional OOP way:
/**
* @param {Array} dictionary An array of words to be inserted in the trie
*/
var Trie = function (dictionary) {
this.hash = {};
this.terminal = false;
};
/**
* adds a word to the trie
* @param {string} word
*/
Trie.prototype.addWord = function (word) {
var node = this.hash;
if (word.length > 0) {
var c = word.charAt(0);
if (!node[c]) {
node[c] = new Trie();
}
if (word.length == 1) {
node[c].terminal = true;
} else {
node[c].addWord(word.substring(1));
}
}
};
/**
* searchs for a word, returns true if it's contained
* in the trie, false otherwise.
* @param {string} word
* @param {boolean=} prefix search for prefix
* @param {Trie=} root the root from where to start searching
* @returns {boolean|Trie}
*/
Trie.prototype.searchWord = function (word, prefix, root) {
if (word.length > 0) {
var c = word.charAt(0),
node = (root || this.hash[c]);
if (node) {
if (word.length > 1)
return node.searchWord(word.substring(1), prefix);
return (prefix ? node : node.terminal);
}
}
return false;
};
Easy, simple, OOP. The problem was that every time I had to re-create the trie because it was not easily serializable. Let's fix that.
/**
* trie.js
* A simple trie implementation
*
* (c) 2012, Rolando Abarca
*/
/**
* @namespace
*/
var Trie = Trie || {};
/**
* creates a new Trie object
* @returns {Object}
*/
Trie.empty = function () {
return {
branches: {},
terminal: false
};
};
/**
* Inserts a word in the trie. The same function is used to recursively
* insert the rest of the word in the trie
*
* @param {Object} trie
* @param {string} word
*/
Trie.insertWord = function (trie, word) {
if (word.length > 0) {
var c = word.charAt(0);
if (!trie.branches[c]) {
trie.branches[c] = Trie.empty();
}
if (word.length == 1) {
trie.branches[c].terminal = true;
} else {
Trie.insertWord(trie.branches[c], word.substring(1));
}
}
};
/**
* searchs for a word in a trie, returns true if it's contained
* in the trie, false otherwise.
*
* @param {Object} trie
* @param {string} word
* @param {boolean=} prefix search for prefix
* @returns {boolean|Trie}
*/
Trie.findWord = function (trie, word, prefix) {
if (word.length > 0) {
var c = word.charAt(0),
node = trie.branches[c];
if (node) {
if (word.length > 1)
return Trie.findWord(node, word.substring(1), prefix);
return (prefix ? node : node.terminal);
}
}
return false;
};
Now I can even create the trie offline and still use it to search for a word in the dictionary. The "trie" object is just a regular javascript object holding only current node information (the letter in that node and the branches). By separating it this way, I could spawn a WebWorker and create a huge trie in a new thread, and then send back the already created trie back to the main thread. I can do this because the object is serializable :)
That was simple, but how would you approach multiple composition, in order to simulate polymorphism, or interface-like implementations? Just compose the different prototypes! They're just sets of properties that you can compose any way you want.
The idea is to think stop thinking of "Classes" and think about different kinds of collections. In the end, classes provide you with the same functionality, but by separating the properties from the functionality, you get to take advantage of using the real power of prototype based inheritance in javascript, as opposed to force a clumsy model that will always work half-way.
Don't get me wrong: if you're comfortable using an OOP in javascript, just keep doing it, but if you're just starting your project, you might want to flirt with the idea of actually using prototype based composition.
From my part, I have my personal project already written using an OOP perspective, but I plan to move it away from that, since that way I will be able to make it more flexible and robust. Also, the API will be cleaner and more consistent, something you always want when creating a framework :) As an added bonus, object creation using simpler data-only objects is way faster. Again, take it with a grain of salt.