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?
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:
var TextNode1 = function(txt)
{
this.contents = txt;
this.toString = function()
{
return this.contents;
}
}
var textNode1 = new TextNode1("This is not text!");
document.write(textNode1);
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]").
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.
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.
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 += "" + this.tagStr + ">";
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.