Reliable Software Logo
Home > JavaScript Tutorial

JavaScript Tutorial, Part 3

We've seen how JavaScript deals with inheritance through the prototype chain. There are different aspect of inheritance in OO languages. One is related to subtyping and is known as the Liskov substitution principle (one can substitute a derived object where a base object is expected). Since JavaScript is not statically type-checked, this aspect is moot. What's left is the sharing of behavior.

Inheritance

Let's go back to the HTML tree example. There are two major kinds of nodes, the ones that can have children, and the ones that can't. They have different syntax: the former enclose their contents between a pair of tags, like <table>...</table>, and the latter use a single tag, like <img/>. They do share some behavior though: they both may specify HTML properties. An image tag usually has the src property, and the table tag may specify border, cellpadding, or cellspacing. It's natural to abstract this behavior into a base object, Node.

Base object

The Node constructor doesn't do anything, but I added a method addProp (add property) to its prototype. An HTML property is a pair of (name, string value), as in border="1". The most natural data structure to store such pairs is an associative array. It so happens that every Object is an associative array of (property name, property value). The syntax, obj.property is just syntactic sugar for obj["property"], which is exactly what we need. You can see this syntax in this.properties[name], where name is either a string or convertible to a string, in the example below.

var Node = function() {}

Node.prototype.addProp = function(name, value)
{
    if (this.properties == null)
        this.properties = new Object();
    this.properties[name] = value;
}

I will call the single-tag node SoloNode. Since I want to be able to add HTML properties to it without duplicating the addProp code, I'll stick a Node into its prototype chain. Notice how it's done: I create a new Node and attach it to the prototype property of the constructor. This single Node will be shared by all objects created using this constructor. The lookup for addProp will go up the prototype chain of such an object and find it in the Node. The lookup for toString would go ever higher, to an empty object that's the prototype of Node and then to it's prototype, Object.prototype, where toString would be found. But I don't want the default toString, so I add my own version of toString to the prototype of SoloNode.

var SoloNode = function(tagStr)
{
    this.tagStr = tagStr;
}
SoloNode.prototype = new Node();

SoloNode.prototype.toString = function()
{
    var result = "<"  + this.tagStr;
    for (name in this.properties)
    {
        result += " " + name + "=" + '"' + this.properties[name] + '"';
    }
    result += "/>";
    return result;
}

And now for actual code reuse. I rewrite TagNode to also inherit from Node. This way I'll be able to addProp to a TagNode, without replicating its code.

var TagNode = function(tagStr)
{
    this.tagStr = tagStr;
}
TagNode.prototype = new Node();

TagNode.prototype.addText = function(str)
{
    this.addChild(new TextNode(str));
}

TagNode.prototype.toString = function()
{
    var result = this.startTag();
    if (this.children != null)
        result += this.children.join(" ");
    result += this.endTag();
    return result;
}

TagNode.prototype.addChild = function(child)
{
    if (this.children == null)
        this.children = new Array();
    this.children.push(child);
}

TagNode.prototype.startTag = function()
{
    var result = "<"  + this.tagStr;
    for (name in this.properties)
    {
        result += " " + name + "=" + '"' + this.properties[name] + '"';
    }
    result += ">";
    return result;
}

TagNode.prototype.endTag = function()
{
    return "</" + this.tagStr + ">";
}

We are ready now to dynamically create an HTML table. The top node is TagNode("table"). I set its border and cellpadding HTML properties. Then I create the first row, TagNode("tr"), and add to it column headers, TagNode("th"). I add this row to the table then proceed with the second row. The image in the second row is created using SoloNode("img"), with two HTML properties.

var table = new TagNode("table");
table.addProp("border", "1");
table.addProp("cellpadding", "4");
  var row1 = new TagNode("tr");
    var hdr1 = new TagNode("th");
    hdr1.addText("Author");
  row1.addChild(hdr1);
    var hdr2 = new TagNode("th");
    hdr2.addText("Title");
  row1.addChild(hdr2);
    var hdr3 = new TagNode("th");
    hdr3.addText("Cover");
  row1.addChild(hdr3);
table.addChild(row1);
  var row2 = new TagNode("tr");
    var d1 = new TagNode("td");
    d1.addText("Bartosz Milewski");
  row2.addChild(d1);
    var d2 = new TagNode("td");
      var link = new TagNode("a");
      link.addProp("href", "http://www.relisoft.com/book");
      link.addText("C++ In Action");
    d2.addChild(link);
  row2.addChild(d2);
    var d2 = new TagNode("td");
      var img = new SoloNode("img");
      img.addProp("src", "images/cpp_cover.jpg");
      img.addProp("alt", "cover image");
    d2.addChild(img);
  row2.addChild(d2);
table.addChild(row2);

document.write(table);
// alert(table);

This is what the output looks like:

Again, since I don't need to generate data for my table dynamically, the same job could have easily been done directly in HTML. I must say though that I copied the HTML code below from a message box generated by adding alert(table) at the end of the script. This way I knew all tags would automatically match.

<table border="1" cellpadding="4">
  <tr>
    <th>Author</th> 
    <th>Title</th> 
    <th>Cover</th>
  </tr> 
  <tr>
    <td>Bartosz Milewski</td> 
    <td><a href="http://www.relisoft.com/book">C++ In Action</a></td> 
    <td><img src="images/cpp_cover.jpg" alt="cover image"/></td>
  </tr>
</table>

Calling base constructors

In most OO languages we are used to being able to pass parameters to the base constructor of an object. In C++, for instance, you do it in the constructor of the derived class:

Derived::Derived(int a)
  : Base(a)
{}

In JavaScript things are a bit complicated by the fact that we need to pass the this argument to the base constructor explicitly. Fortunately, every function object has a method call that can be used for just that purpose. Here I'm subclassing TagNode to create a specialized version of the table node, MyTable. I pass the tag name, "table", to its base constructor.

var MyTable = function()
{
  TagNode.call(this, "table");
  this.addProp("border", "1");
  this.addProp("cellspacing", "0");
  this.addProp("cellpadding", "4");
}
MyTable.prototype = new TagNode();

Here's another table created using MyTable constructor:

var myTable = new MyTable();
  var row1 = new TagNode("tr");
    var hdr1 = new TagNode("th");
    hdr1.addText("Location");
  row1.addChild(hdr1);
    var hdr2 = new TagNode("th");
    hdr2.addText("Blog");
  row1.addChild(hdr2);
myTable.addChild(row1);
  var row2 = new TagNode("tr");
    var d1 = new TagNode("td");
    d1.addText("Wordpress");
  row2.addChild(d1);
    var d2 = new TagNode("td");
      var link = new TagNode("a");
      link.addProp("href", "http://bartoszmilewski.wordpress.com");
      link.addText("Bartosz Milewski's Programming Cafe");
    d2.addChild(link);
  row2.addChild(d2);
myTable.addChild(row2);

document.write(myTable);

In my opinion, creating a dynamic table using OO techniques is more flexible and less error-prone than doing it with repeated use of document.write(). It's also easy to extend the classes I described to do more error checking. For instance, the addChild method could be specialized for a Table node to check if the child is indeed a table row (testing the tagStr property). Similarly addChild of a TableRow node could check if the children have either the "td" or "th" tags. One could also specialize addProp methods for various types of nodes, etc.

Similar techniques can be applied to HTML forms and many other structures dynamically generated from external data. The up front effort in designing JavaScript classes will pay over and over in the ease of creation and maintenance of web pages.

PrevBack: Constructors and Prototypes.