Prototyping with JavaScript


I think I’ve finally managed to get a handle on prototyping with JavaScript, after all that lisping and jQuerying it turned out to be a formality in the end. I have to thank Steve for finally getting my ass off the couch though.

At first I started thinking about how to do something similar to a classical OO system and I found an example, quite a lot isn’t it? It didn’t seem to rhyme very well with what Steve wrote either, about how instances inherit instances.

Let’s take a look at something very simple and also let’s start from the very beginning in case you’re really indoctrinated and find this hard to wrap your head around. Don’t sweat it if you do, I’ve had major problems too.

First of all we have to realize that a function is not just a function in the traditional sense, it is but at the same time it isn’t, it’s just that the choice of wording is horrible, anything but function would have been better. Anyway, the function keyword in JavaScript can basically function as the class word in other languages. Lets take a look at a simple example:

function Human(name, age){
  this.name = name;
  this.age = age;
  this.isRetired = function(){ 
    return this.age > 65 ? true : false; 
  }
}
Harry = new Human("Harry", 50);
document.writeln(Harry.isRetired());

So the function can be instantiated, super weird I know, imagine I had written class Human… instead or something. Note also the assignment of a function to a member variable in the form of isRetired, completely legal, it seems that an unnamed function() can be assigned to a variable just like everything else. That’s why we can do the following too:

Harry.toStr = function(){
  return "Name: " + this.name + ", Age: " + this.age;
} 

document.writeln(Harry.toStr());

Cool, we get his name and age alright. Now try this:

Barry = new Harry.constructor("Barry", 70); // Same as new Human("Barry", 70);
document.writeln(Barry.toStr());

Oops, “Barry.toStr is not a function”, that’s because we’re really just creating another human, since toStr was added to Harry, Barry won’t have access to this functionality. To solve this problem the famous prototype keyword can be used:

Human.prototype.toStr = function(){
  return "Name: " + this.name + ", Age: " + this.age;
}
Barry = new Harry.constructor("Barry", 70); // Same as new Human("Barry", 70);
document.writeln(Barry.toStr());

We basically added toStr() to the Human “class” here, or “prototype” in this case, that is what that initial Human function is, a prototype, or blueprint if you’re an architect. Remember the above way of creating a new Human object with the help of instance.constructor(), it will become very important shortly.

Keep the above prototype statement and add this below:

Harry.toStr = function(){
  return "Harry here. Name: " + this.name + ", Age: " + this.age;
} 
document.writeln(Harry.toStr());

So the prototype was locally overridden like we would expect. Let’s take a look at how a simple inheritance system based on instances instead of prototyping can be constructed, if you want classical inheritance through prototyping mania, by all means, follow the above link and dig in :)

Object.prototype.extend = function(o) {
  var obj     = new this.constructor();
  var me      = eval(uneval(this));
  
  for(var key in me)
    obj[key] = me[key];
    
  for(var key in o)
    obj[key] = o[key];
  
  obj.parent = o.parent ? o.parent : eval(uneval(this));
       
  return obj;
}

The above is a juxtaposition of Gareth Heyes and Keith Devens clone examples.

First of all, Object is the uber template if you will. Everything we do inherits from this base construct. That’s why we can add the above extend function to everything by prototyping it.

First we create a new object with the help of the constructor of the “parent” object, in this case a new Human, the point is that we lay the foundation of our new object with everything that has been prototyped/attached to the Human prototype/class/template/blueprint. Next we create a perfect clone of for instance Harry, all Harry’s properties will then be copied to the new object, if Harry for instance implements his own toStr() method the new object will get it too.

The second loop will in turn overwrite or add more attributes from an explicitly passed object, you’ll soon see what it looks like.

The last line lets us override the parent attribute with something we pass into the process, if not we simply use the extended instance as the parent.

The above makes the most sense since we are overwriting something very basic, in the prototype, with something more specific, in the instance. Before you run the below test make sure you have Harry’s creation line, the prototype toStr and Harry’s version of it uncommented:

Barry = Harry.extend({name: "Barry", age: 70});
document.writeln(Harry.toStr());
document.writeln(Barry.toStr());
document.writeln(Barry.isRetired());

As expected, the prototyped toStr method got overwritten by Harry’s version and that is the version Barry got, beware!

Let’s move on to some American football, make sure you comment out Harry’s toStr before you run this, we use only the prototyped toStr from now on:

NFLPlayer = Harry.extend({
  out: function(){
     document.writeln(this.toStr() + ", Team: " + this.team);
  },
  isRetired: function(){ 
    return this.age > 35 ? true : false; 
  }
});

Jason =  NFLPlayer.extend({
  name: "Jason", 
  age: 25, 
  team: "Chicago Bears",
  injured: true
});

Jason.out();

So out makes use of the toStr that has been added to the Human prototype, great, we can reuse code. Note that we retire NFL players at the age of 35 instead of 65 which was set in the prototype.

Jason.isInjured = function(){
  return this.injured;
}

Bill = Jason.extend({name: "Bill", age: 27, injured: false});

document.writeln(Bill.isInjured());

So we follow up with adding another function to the Jason instance and then we create Bill from him, and of course Bill will now have the isInjured method too. In case the parent information is wanted it can of course be retrieved:

document.writeln(Bill.parent.name);

Let’s create a coach:

Coach = NFLPlayer.extend({name: "Bob", age: 45});
document.writeln(Coach.isRetired());

Finally, we got to try out the isRetired method :)

Note that the above way of doing things will work even if the parent object is destroyed since we have cloned everything, try it out:

Bill = Jason.extend({name: "Bill", age: 27, injured: false});
Jason = '';
document.writeln(Bill.name);
document.writeln(Bill.isInjured());
document.writeln(Bill.parent.name);

If a real reference is needed the parent can be set explicitly:

Bill = Jason.extend({name: "Bill", age: 27, injured: false, parent: Jason});
Jason.age = 32;
document.writeln(Bill.name);
document.writeln(Bill.isInjured());
document.writeln(Bill.parent.age);

Even though we only do one parent in this example it should be evident that we can easily work with an arbitrary number of parents simply by copying their functions to the new instance in a “factory” fashion. I’ll leave that up to you to :)

Further reading on the topic of multiple inheritance: More OO in PicoLisp.

Related Posts

Tags: , ,