The document object model in JavaScript: Structure and trees

1. Document Structure

You can imagine an HTML document as a nested set of boxes. Tags such as <body> and </body> enclose other tags, which in turn contain other tags or text. Here’s the example document from the previous chapter:

<!doctype html>

<html>

<head>

<title>My home page</title>

</head>

<body>

<h1>My home page</h1>

<p>Hello, I am Marijn and this is my home page.</p>

<p>I also wrote a book! Read it <a href=”http://eloquentjavascript.net”>here</a>.</p>

</body>

</html>

This page has the following structure:

The data structure the browser uses to represent the document follows this shape. For each box, there is an object, which we can interact with to find out things such as what HTML tag it represents and which boxes and text it contains. This representation is called the Document Object Model, or DOM for short.

The global binding document gives us access to these objects. Its documentElement property refers to the object representing the <html> tag. Since every HTML document has a head and a body, it also has head and body properties, pointing at those elements.

2. Trees

Think back for a moment to the syntax trees from “Parsing” on page 203. Their structures are strikingly similar to the structure of a browser’s docu­ment. Each node may refer to other nodes, children, which in turn may have their own children. This shape is typical of nested structures where elements can contain subelements that are similar to themselves.

We call a data structure a tree when it has a branching structure, has no cycles (a node may not contain itself, directly or indirectly), and has a single, well-defined root. In the case of the DOM, document.documentElement serves as the root.

Trees come up a lot in computer science. In addition to representing recursive structures such as HTML documents or programs, they are often used to maintain sorted sets of data because elements can usually be found or inserted more efficiently in a tree than in a flat array.

A typical tree has different kinds of nodes. The syntax tree for the Egg language had identifiers, values, and application nodes. Application nodes may have children, whereas identifiers and values are leaves, or nodes with­out children.

The same goes for the DOM. Nodes for elements, which represent HTML tags, determine the structure of the document. These can have child nodes. An example of such a node is document.body. Some of these children can be leaf nodes, such as pieces of text or comment nodes.

Each DOM node object has a nodeType property, which contains a code (number) that identifies the type of node. Elements have code 1, which is also defined as the constant property Node.ELEMENT_NODE. Text nodes, repre­senting a section of text in the document, get code 3 (Node.TEXT_NODE). Com­ments have code 8 (Node.COMMENT_NODE).

Another way to visualize our document tree is as follows:

The leaves are text nodes, and the arrows indicate parent-child relation­ships between nodes.

3. The Standard

Using cryptic numeric codes to represent node types is not a very JavaScript­like thing to do. Later in this chapter, we’ll see that other parts of the DOM interface also feel cumbersome and alien. The reason for this is that the DOM wasn’t designed for just JavaScript. Rather, it tries to be a language- neutral interface that can be used in other systems as well—notjust for HTML but also for XML, which is a generic data format with an HTML-like syntax.

This is unfortunate. Standards are often useful. But in this case, the advantage (cross-language consistency) isn’t all that compelling. Having an interface that is properly integrated with the language you are using will save you more time than having a familiar interface across languages.

As an example of this poor integration, consider the childNodes prop­erty that element nodes in the DOM have. This property holds an array-like object, with a length property and properties labeled by numbers to access the child nodes. But it is an instance of the NodeList type, not a real array, so it does not have methods such as slice and map.

Then there are issues that are simply poor design. For example, there is no way to create a new node and immediately add children or attributes to it. Instead, you have to first create it and then add the children and attri­butes one by one, using side effects. Code that interacts heavily with the DOM tends to get long, repetitive, and ugly.

But these flaws aren’t fatal. Since JavaScript allows us to create our own abstractions, it is possible to design improved ways to express the operations you are performing. Many libraries intended for browser programming come with such tools.

4. Moving Through the Tree

DOM nodes contain a wealth of links to other nearby nodes. The following diagram illustrates these:

Although the diagram shows only one link of each type, every node has a parentNode property that points to the node it is part of, if any. Likewise, every element node (node type 1) has a childNodes property that points to an array-like object holding its children.

In theory, you could move anywhere in the tree using just these parent and child links. But JavaScript also gives you access to a number of addi­tional convenience links. The firstChild and lastChild properties point to the first and last child elements or have the value null for nodes without children. Similarly, previousSibling and nextSibling point to adjacent nodes, which are nodes with the same parent that appear immediately before or after the node itself. For a first child, previousSibling will be null, and for a last child, nextSibling will be null.

There’s also the children property, which is like childNodes but contains only element (type 1) children, not other types of child nodes. This can be useful when you aren’t interested in text nodes.

When dealing with a nested data structure like this one, recursive func­tions are often useful. The following function scans a document for text nodes containing a given string and returns true when it has found one:

function talksAbout(node, string) {

if (node.nodeType == Node.ELEMENT_NODE) {

for (let i = 0; i < node.childNodes.length; i++) {

if (talksAbout(node.childNodes[i], string)) {

return true;

}

}

return false;

} else if (node.nodeType == Node.TEXT_NODE) {

return node.nodeValue.indexOf(string) > -1;

}

}

console.log(talksAbout(document.body, “book”));

// → true

Because childNodes is not a real array, we cannot loop over it with for/of and have to run over the index range using a regular for loop or use Array.from.

The nodeValue property of a text node holds the string of text that it represents.

Source: Haverbeke Marijn (2018), Eloquent JavaScript: A Modern Introduction to Programming, No Starch Press; 3rd edition.

Leave a Reply

Your email address will not be published. Required fields are marked *