Understanding Prototype-based Inheritance in JavaScript
Posted on Dec 05, 2011
Understanding Prototype-based Inheritance in JavaScript
Recently I started on a quest to learn JavaScript, and I mean really learn JavaScript. Sure, for many years I have hacked around in JavaScript. I even did a good deal of work with jQuery in my past (in fact, some of my most trafficked posts to this day are jQuery posts from a few years back). However, this time I wanted to approach it from the standpoint of actually understanding the language rather than just hacking together some cool DOM manipulation. In doing so, I found my first mental block was trying to understand the concept of prototype-based inheritance that JavaScript uses and, following on that, getting a grip on the dynamic nature of the language.
This will hopefully be the first in a series of posts where I share my experiences trying to learn serious programming in JavaScript. Keep in mind that I make no claim on being a JavaScript guru. In fact, if any JavaScript gurus read this, I’d love to hear feedback to clear up any misconceptions I may have. I also will try to provide comparison examples, which will be generally in ActionScript since it’s what I know. Hopefully sharing my experiences will help any of you who may be taking a similar journey.
Prototype and Portal?
Let me explain. I am a fan of the Portal video games. While I was trying to think about the concept of prototype based inheritance, I kept thinking about the scene from Portal 2 where you replace the proper turret from which all the turrets are duplicated with the reject turret, thereby ensuring that all turrets built are now copies of the reject turret (not a gamer? Get the full skinny on Portal turrets in this video).
In essence, we have an existing object (a turret in this case) that all other objects of that type are cloned from and changing that existing object thereby affects all the clones. I perceive as traditional class-based inheritance more as a blueprint system where all objects are created following the same blueprint wherein the blueprint cannot be changed but can only be amended with additional blueprints. Which reminds me, coming to grips with prototype based programming in JavaScript seems to go hand in hand with accepting the dynamic nature of the language (something ActionScript purists seem loath to do). As with any analogy, neither of these is a perfect explanation but the mental model helped me at least.
Example Applications - Building Portal Turrets with Heat Seeking Missiles
Following on my Portal analogy above, I decided to build a simple application to try to illustrate the differences between prototype based inheritance in JavaScript and the traditional class-based inheritance of ActionScript 3. In this example, I will build a Turret object that has some properties and methods you might expect from a Portal turret, however I would like to make it far deadlier by modding it to add on heat seeking missiles. First let’s look at the ActionScript version.
In ActionScript
To begin with I created a very basic Turret class that contains a number of simple properties and methods that you might expect.
package
{
public class Turret
{
public var machineGun:MachineGun = new MachineGun();
public var laserEye:Boolean = true;
public var isEnemyInSight:Boolean = false;
private var isFiring:Boolean = false;
private var isKnockedOver:Boolean = false;
private var motionDetectedArr:Array = ["Target acquired","There you are","I see you","Preparing to dispense product","Activated"];
private var knockedOverArr:Array = ["Critical error","Shutting down","I don't hate you","Hey, hey, hey","Malfunctioning"];
private var ignoredArr:Array = ["Are you still there"];
public function Turret()
{
}
public function arm():String {
return machineGun.weaponType + " armed";
}
public function fire():void {
isFiring = true;
trace("firing");
}
public function motionDetected():String {
if (isKnockedOver)
return "I am knocked over";
isEnemyInSight = true;
fire();
return motionDetectedArr[Math.floor(Math.random() * motionDetectedArr.length)];
}
public function knockedOver():String {
isFiring = false;
isEnemyInSight = false;
isKnockedOver = true;
return knockedOverArr[Math.floor(Math.random() * knockedOverArr.length)];
}
public function ignored():String {
if (isKnockedOver)
return "I am knocked over";
isEnemyInSight = false;
isFiring = false;
return ignoredArr[Math.floor(Math.random() * ignoredArr.length)];
}
}
}
As with all the turrets in Portal, my turret is armed with a MachineGun which is just a simple class containing a type string.
package
{
public class MachineGun
{
public var weaponType:String = "Machine Gun";
public function MachineGun()
{
}
}
}
Now, I decided that a machine gun just simply isn’t destructive enough, so I created a HeatSeekingMissile and want to arm my Turret with it. From a code perspective, the HeatSeekingMissile is the same as the MachineGun (remember this is just for illustrative purposes).
package
{
public class HeatSeekingMissile
{
public var weaponType:String = "Heat Seeking Missiles";
public function HeatSeekingMissile()
{
}
}
}
Now in a traditional class based inheritance like AS3, I cannot simply mod my Turret and add a HeatSeekingMissile but instead need to extend Turret with a HeatSeekingTurret class that adds the new weapon (yes, for OO purists, I could have made both a type of weapon and had the turret be composed of an array of weapons but that’s not really the point of this example). Here is our new HeatSeekingTurret.
package
{
public class HeatSeekingTurret extends Turret
{
public var heatSeekingMissile:HeatSeekingMissile = new HeatSeekingMissile();
public function HeatSeekingTurret()
{
super();
}
override public function arm():String {
return heatSeekingMissile.weaponType + " and " + super.arm();
}
}
}
Next, let’s put a simple sample application together to bring all the pieces together and test it all out. The sample simply outputs the text generated by these methods into a TextInput and will swap out our Turret with a HeatSeekingTurret when the proper button is clicked. You can run the application here.
<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx" minWidth="955" minHeight="600" creationComplete="creationCompleteHandler(event)">
<fx:Script>
<![CDATA[
import mx.events.FlexEvent;
[Bindable]
private var turret:Turret = new Turret();
private function motionDetected():void {
responseTxt.text = turret.motionDetected();
}
private function knockedOver():void {
responseTxt.text = turret.knockedOver();
}
private function ignored():void {
responseTxt.text = turret.ignored();
}
private function creationCompleteHandler(event:FlexEvent):void
{
responseTxt.text = turret.arm();
}
private function modTurret():void {
turret = new HeatSeekingTurret();
responseTxt.text = turret.arm();
trace(turret is Turret);
}
]]>
</fx:Script>
<fx:Declarations>
<!-- Place non-visual elements (e.g., services, value objects) here -->
</fx:Declarations>
<s:Label x="49" y="45" text="Response:"/>
<s:TextInput id="responseTxt" x="123" y="35" width="300"/>
<s:Button x="49" y="81" label="Motion Detected" click="motionDetected()"/>
<s:Button x="162" y="81" label="Knocked Over" click="knockedOver()"/>
<s:Button x="267" y="81" label="Ignored" click="ignored()"/>
<s:Button x="345" y="81" label="Heat Seeking Missiles!" click="modTurret()"/>
</s:Application>
One important note is that I now have two instances of a Turret, one with HeatSeekingMissiles and my original one without. Obviously, creating an instance of my HeatSeekingTurret does not actually affect any of the existing Turrets that I previously built.
In JavaScript
The code in JavaScript/HTML is surprisingly similar in many respects to the code in ActionScript/MXML. One aside though, there seemed to be no real consensus on how to script your classes in JavaScript. I found tons of examples, not all of which seemed to even work in my browser (which, for what it’s worth, I am using Chrome on a PC for testing this simple example in case you find problems elsewhere). I ended up basing my classes off the code generated by CoffeeScript which is a project I am personally very intrigued by. I also put my three simple classes into a single file since there is no requirement in JavaScript to split them up (and the code was simple enough to not warrant it). Here is my Turret, MachineGun and HeatSeekingMissile equivalents in JavaScript.
var HeatSeekingMissile = (function() {
function HeatSeekingMissile() {
this.weaponType = "heat seeking missile";
}
return HeatSeekingMissile;
})();
var MachineGun = (function() {
function MachineGun() {
this.weaponType = "machine gun";
}
return MachineGun;
})();
var Turret = (function() {
function Turret() {
this.machineGun = new MachineGun();
this.laserEye = true;
this.isEnemyInSight = false;
this.isFiring = false;
this.isKnockedOver = false;
this.motionDetectedArr = ["Target acquired","There you are","I see you","Preparing to dispense product","Activated"];
this.knockedOverArr = ["Critical error","Shutting down","I don't hate you","Hey, hey, hey","Malfunctioning"];
this.ignoredArr = ["Are you still there"];
}
Turret.prototype.arm = function() {
return this.machineGun.weaponType + " armed";
};
Turret.prototype.fire = function() {
this.isFiring = true;
console.log("firing");
};
Turret.prototype.motionDetected = function() {
if (this.isKnockedOver) {
return "I am knocked over";
}
this.isEnemyInSight = true;
this.fire();
return this.motionDetectedArr[Math.floor(Math.random() * this.motionDetectedArr.length)];
};
Turret.prototype.knockedOver = function() {
this.isFiring = false;
this.isEnemyInSight = false;
this.isKnockedOver = true;
return this.knockedOverArr[Math.floor(Math.random() * this.knockedOverArr.length)];
};
Turret.prototype.ignored = function() {
if (this.isKnockedOver) {
return "I am knocked over";
}
this.isEnemyInSight = false;
this.isFiring = false;
return this.ignoredArr[Math.floor(Math.random() * this.ignoredArr.length)];
};
return Turret;
})();
At this point, as you can see from the code, other than some mostly minor syntactical differences the AS and JS versions are quite similar. One important difference however is that I am setting the functions into the object’s prototype property. As I understand it, this simply means that any object that has a Turret as its prototype will inherit these methods (or properties) even though they were not declared in the constructor. To be clear, those properties I explicitly declared in my constructor are also part of the prototype.
Now let’s take a look at the view template. I decided to eschew using any frameworks such as jQuery as this example is simple and it might distract from the point of the exercise. In reviewing the code below you will notice the only real difference between it and the MXML view above is in my method to mod my turret by adding HeatSeekingMissiles. Let’s review the code first.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>Prototype Sample</title>
<script type="text/javascript" src="turret.js"></script>
<script type="text/javascript">
var turret = new Turret();
var rTxt;
function load() {
rTxt = document.getElementById('responseTxt');
rTxt.value = turret.arm();
}
function motionDetected() {
rTxt.value = turret.motionDetected();
}
function knockedOver() {
rTxt.value = turret.knockedOver();
}
function ignored() {
rTxt.value = turret.ignored();
}
function modTurret() {
Turret.prototype.heatSeekingMissile = new HeatSeekingMissile();
Turret.prototype.arm = function() {
return this.heatSeekingMissile.weaponType + " and " + this.machineGun.weaponType + " armed";
};
rTxt.value = turret.arm();
console.log(turret instanceof Turret);
newTurret = new Turret();
console.log(newTurret.arm());
}
</script>
</head>
<body onload="load()">
Response: <input name="responseTxt" id="responseTxt" type="text" size="60" /><br />
<input type="button" name="motionBtn" value="Motion Detected" onclick="motionDetected()" />&nbsp;
<input type="button" name="knockedBtn" value="Knocked Over" onclick="knockedOver()" />&nbsp;
<input type="button" name="ignoredBtn" value="Ignored" onclick="ignored()" />&nbsp;
<input type="button" name="missilesBtn" value="Heat Seeking Missiles!" onclick="modTurret()" />
</body>
</html>
Let’s focus on the modTurret() method. You will notice here that I am not extending my Turret class with a HeatSeekingTurret but in fact just “hacking” HeatSeekingMissiles onto my existing Turret prototype. The interesting thing here is that not only do future Turrets now have HeatSeekingMissiles but all of my previous Turrets do as well (including the one I had previously instantiated). Much like the scene I discussed earlier from Portal where (plot holes aside) GlaDOS can’t seem to find a functional turret to kill you with because you replaced her prototype with a dud, in my case every Turret I ever created is now deadlier than ever!
If you’d like to test out the JavaScript version of this sample, you can run it here. Note that I did leave the console.log() calls in there in case your running on a browser where it isn’t supported.
As I said earlier, I feel that understanding prototype based programming in JavaScript goes hand in hand with understanding the dynamic nature of the language. So let’s look at an alternative here to illustrate that. Imagine instead I had done my modTurret() method differently.
function modTurret() {
turret.heatSeekingMissile = new HeatSeekingMissile();
turret.arm = function() {
return this.heatSeekingMissile.weaponType + " and " + this.machineGun.weaponType + " armed";
};
rTxt.value = turret.arm();
console.log(turret instanceof Turret);
newTurret = new Turret();
console.log(newTurret.arm());
}
In the above case, I have still created a Turret with HeatSeekingMissiles but prior Turrets other than the instance I modified are unchanged. In addition, future Turrets still only carry MachineGuns. This is because I have not modified the prototype but rather only the single Turret instance that I had previously created.
Next Steps
In my opinion, the more I understand the underlying concepts within JavaScript and comparing them to concepts in languages I already know, the better I can appreciate the power and intricacies of the language and how to create complex applications with it. Hopefully this discussion has been helpful to some readers. I know the exercise was helpful to me to better understand some of the underlying concepts in the JavaScript language. If anyone has constructive feedback, please share. I will continue to blog these as I continue my own learning. Some topics I plan to cover next are livig without a compiler (specifically dynamic vs. static typing), namespaces and events. If you have any suggestions for topics you’d like to see me discuss, let me know as I am happy to take many detours along this journey.
Special thanks to my friends Kyle Hayes and Peter Bell for helping me review this post.
Comments
I'm doing about the same thing you're doing, and I'm about a quarter way through "Javascript: The Good Parts" by Douglas Crockford.
One of the biggest favors I did for myself was to throw out the notion of class definitions/blueprints and look at Javascript pretending that stuff didn't exist.
Once you realize that there's no such thing, you start realizing what's going on. Everything's an instance object created at runtime. Yes, you can create an instance and PRETEND it's a class definition, but that's all you're doing is pretending.
Prototypes are the same, they aren't tied to class definitions, cause there's no such thing as a class definition. All prototypes are, is a nested hierarchy of properties.
Object A has a buncha methods, and also has a prototype object because it's an object, and all objects have prototypes. You can start putting methods in the prototype, maybe all the methods from Object B. And then you can put a bunch of methods in the prototype of Object C.
You might, by classical language definition say that Object A extends Object B extends Object C. But it's really your choice to use this mess of inheritance as a blueprint or an instance object.
That's why everyone does everything differently. I came from Actionscript and looked for THE way to do this stuff, as class definitions are so rigid that way. Everybody did things in a different way, so I was all frustrated.
I ended up attacking it from a different angle which was to relearn Objects, Functions, and Scope the Javascript way. When you start basic like that and throw everything else out it's very illuminating. And only then (for me), was I able to make the connections between what I wanted to do with classical programming and Javascript!
Posted By Ben Farrell / Posted on 12/05/2011 at 11:09 AM
I find it very entertaining that a fellow ActionScript developer is unfamiliar with the prototype chain, considering that it was the only way to do anything in ActionScript before classes were introduced in Flash 7 (MX 2004).
If you want to learn a lot of techniques and best practices, have a read through old FlashKit, FlashCoders, and other forums and blogs with dates before 2004.
Of course, since you want to focus on modern JavaScript, it may be less confusing if you limit your research to resources about it instead of comparing it to ActionScript in any way.
Take a look at the ECMA spec for the nuts and bolts: http://www.ecma-international.org/publications/standards/Ecma-262.htm
Posted By Martin / Posted on 12/05/2011 at 7:15 PM
Since you work for Adobe, you should look at the ActionScript 1.0 documentation for an explanation of inheritance and the prototype chain: http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/2/help.html?content=00000526.html
Posted By Martin / Posted on 12/05/2011 at 8:05 PM
@Martin - thanks for your comment, though am I wrong in sensing some hostility? If so, can I ask why? For what its worth, I didn't do a lot of AS2. I was focused mostly on server side work at the time (though I did some minimal AS2 and Flex 1.5 work). I returned to Flash/ActionScript around Flex 2 which was AS3.
Posted By Brian Rinaldi / Posted on 12/06/2011 at 5:28 AM
JavaScript is DEAD ! Google is moving to DART :) and it is for good.
Posted By Alexander / Posted on 12/07/2011 at 1:08 AM