Reliable Software Logo
Home > JavaScript Tutorial

JavaScript Tutorial, Part 2

It's time for some more practical examples of JavaScript programming. JavaScript is mostly used in web programming and is frequently used to generate HTML. I've seen a lot of code that generates complex HTML by using nothing more than document.write and string concatentaion. This kind of code is extremely error prone and misses the whole point of JavaScript programing.

When I need to programmatically generate, for instance, an HTML table, I want to create a data structure that describes the table and then output it in one fell swoop. My data structures will guarantee the creation of correct HTML, with all the tags matching.

What is an HTML page if not a serialized tree-like data structure? Why not use trees to represent it?

Constructors

In order to build a tree, I'll need to create a lot of nodes. Instead of building node factories, I'll use a different technique, one that introduces constructors. A constructor is just a function, possibly having access to this. You construct objects by calling new on the constructor.

Here's the constructor of TextNode1. It takes a string argument and stores it in the property contents of the "this" object. It also creates a property toString and initializes it to a function that returns the contents of "this". Where does this "this" come from? The way to call a constructor is through new. Here's what happens when you do it:

  • A fresh empty Object is created (the same kind as the literal {}).
  • The constructor is called with the just allocated Object as "this".
  • The constructor add new properties to "this" if necessary. Here it adds contents and the method toString.
  • The constructed object is returned.
Notice that I can pass the textNode1 object directly to document.write--it converts any object to string by calling its method toString, whether inherited from Object (see next section) or--as in our case--overriden.
var TextNode1 = function(txt)
{
  this.contents = txt;
  this.toString = function()
  {
    return this.contents;
  }
}

var textNode1 = new TextNode1("This is not text!");
document.write(textNode1);

Prototypes

This way of creating objects is a little wasteful. Granted, each TextNode1 must have it's own copy of the property contents, but it doesn't have to have its own copy of the function toString. Methods should not be duplicated. Fortunately there is a place to store methods that is shared between all objects of a given kind. This place is called the prototype.

To understand what a prototype is, you have to understand how properties are looked up. The first place to look for an object's property is the object itself. For instance, if you call textNode1.toString(), the property toString is found on the object textNode1 itself (see top part of Fig. 1). This property was created inside the constructor code. If the constructor hadn't added this property, it would have been looked up in the textNode1's prototype, its prototype's prototype, etc. Conceptually (and in many implementations literally) the prototype chain is accessed through the hidden property __proto__ defined for every object.

The prototype chain for textNode1 consists of an empty Object, TextNode1.prototype, whose prototype is the system-wide Object.prototype.

It is important to understand that all objects created using a given constructor share the same prototype object. They get a reference to this prototype from the special prototype property of the constructor (yes, constructor is a function, but every function is an object too, so it can have properties). Because of this sharing, if you modify the prototype property of a given constructor, all objects created using that constructor will see that modification.

In this example, I create a TextNode object without the specialized method toString.

var TextNode = function(txt)
{
  this.contents = txt;
}

var txt1 = new TextNode("This looks like text.");
var txt2 = new TextNode("And so does this.");
document.write("txt1: " + txt1);
document.write("<br/>txt2: " + txt2);

When txt1 has to be converted to a string by document.write, the toString lookup in the object fails, and the lookup proceeds to the prototype (see lower part of Fig. 1). The default prototype is just an empty Object (TextNode.prototype). This empty object has the prototype, Object.prototype, which happens to have the property toString (it retuns the string "[object Object]").

Modifying the default prototype

Next, I add a special implementation of toString to the prototype property of the TextNode constructor. Remember, this prototype is an Object shared by all TextNode objects. When I display the same txt1 node again, it suddenly finds the new implementation of toString in its prototype.

TextNode.prototype.toString = function()
{
  return this.contents;
}

document.write("txt1: " + txt1);
document.write("<br/>txt2: " + txt2);

It is standard procedure to add methods to the prototype, rather than to individual instances.

More methods

Here's a more elaborate example: I define a TagNode to be a node with an opening and closing HTML tag, and with an optional array of child nodes in between. There are three methods, all added to the prototype.

  • addChild: The first time it's called, it creates the property children and initializes it with an empty Array (one of the built-in types). It then pushes the child node on top of this array.
  • addText: Adds a new TextNode to the child array.
  • toString: Returns a string starting with the opening HTML tag, followed by a concatentation of all child nodes converted to strings, and ending with the closing HTML tag. The join method of an array converts all array elements to strings (calling their toString methods) and concatenates them using a spacer provided as an argument (here it's a single space, " ").
var TagNode = function(tagStr)
{
    this.tagStr = tagStr;
}

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

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

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

I use these nodes to create a small tree. The root of the tree is a paragraph element. I add to it three children: one is a text node, the other a bold node (with a text-node child), and the third one is text again. You can see the result of serializing this tree (that's what the toString method does) in the box below.

var para = new TagNode("p");
para.addText("Now this is ");
  var bold = new TagNode("b");
  bold.addText("text");
para.addChild(bold);
para.addText("!");

document.write(para);

Of course, when the HTML text is known up front, it's much easier to write it directly in HTML, as in:

<p>Now this is <b>text</b>!</p>
The JavaScript method works best for dynamically generated text.


 

BackBack: Object and Function Literals.   NextNext: Inheritance.