Advanced JavaScript Concepts
LUG Programming Course, 28th January 2008
Writing code snippets in Firebug is interesting, but not permanent. As soon as you close the browser, or the tab you were using, all that experimenting is gone forever. The time has come to create a permanent development project.
Overran the lesson by half an hour again, and didn't get as far as creating objects, I stopped short of “The Power of JavaScript”. I'm also a little worried that I'm leaving some students behind. To rectify, I'll add a refresher lesson in a couple of weeks.
Your First Aptana Studio Project
Start Aptana Studio. Yes, I know it takes a while, so I'll wait...
Select File from the menu bar. Then select New and Project..., which opens the New Project Wizard. Under the folder General, select Project, then click on Next. This is going to be the place where we will be doing some experimentation, so we'll call the project Experiments. Uncheck the Use default location check box, and select a folder, such as C:\LUGPC\Experiments on Windows. Finally, click on Finish.
You should now see Experiments in the Project view. If you don't have a Project view, you can open it by selecting Window from the menu bar, then Show view, then click on Project.
Next we'll create some 'stub' files, but first we'll decide on our folder layout:
public/
scripts/
styles/
We'll create a principal folder called public. This is for our 'static' files, into which will go our HTML files. We'll also create two sub folders, one for our JavaScript code, and one for our CSS files.
Within the Project view, right click and select New, then Folder to create the folder hierarchy, and add public.
Next, we'll create a JavaScript file under public/scripts/. Select this folder, then right click, select New, then JavaScript File, and give it the name experimental.js.
Now we'll do the same for our CSS file, this time under public/styles/. Select this folder, then right click, select New, then CSS File, and give it the name experimental.css.
Lastly, we'll create an XHTML file, under public/. Select this folder, then right click, select New, then HTML File, and give it the name experimental.html.
You've probably noticed that Aptana Studio opens the newly created files, and that they already contain some text. We can leave the JavaScript and CSS files alone for the moment, but we need to modify the HTML file. So that we know where we all are, we'll also add line numbering to the editor views; select Window from the menu bar, then Preferences..., then double click on General, to open the sub-options, then double click on Editors, then click on Text Editors, and check the Show line numbers check box. Finally, click on OK.
Modifying the XHTML Document
Aptana Studio creates an HTML 4.01 document by default. We actually want an XHTML 1.0 Transitional document. Now, we can make all our changes just using the editor, but since we're working with an Integrated Development Environment, I'll show you how to make the best use of it, and reduce the amount of typing required.
Aptana Studio has a useful view called Snippets. If you don't already have this view open, select Windows from the menu bar, then Show view, then click on Snippets.
Now click on the start of line 1 of experimental.html, press Shift and the down arrow together, which will highlight the line, then press Enter, which will substitute the first line with a new blank first line, finally, move the cursor up to the start of that line. From the Snippets window, double click on Insert DOCTYPE XHTML 1.0 Transitional. The <DOCTYPE ...> text will be added to our HTML file.
To finish our modification, we need to add the XML namespace attribute to our html tag, on line 2. Move the cursor to the '>' character and press the spacebar. Aptana Studio will display a list of attributes in a popup window. Type x, then double click on xmlns. Next, type either a single or double quote, Aptana Studio will create two characters (which delimit the attribute value), and offer a value to insert; http://www.w3.org/1999/xhtml. Double click on the suggestion, and it will be inserted inside the quotes.
On line 4, there is a meta tag, which also needs changing. Move the mouse over the content attribute, and a small popup will appear, explaining the attribute. Delete the attribute value (including quotes) after the '=' character, then type a single or double quote, and double click on text/html; charset=UTF-8.
Change the title tag contents, on line 5, to LUG Programming Course, Lesson 3, or something similar.
Now we have an XHTML document, which uses
UTF-8 encoding, the default character encoding for XML documents. Press
Ctrl+S to save the file.
Adding Tags to the XHTML Document
Next, we want to add tags which link to our (as yet empty) JavaScript and CSS file. On line 5 add a blank line, then in Snippets, double click Insert CSS <link>. Remove the contents of the href attribute value, taking care to keep the double quotes, then type styles/experimental.css. Now we can quickly check that our link is correct, open the experimental.css file, and on line 2 add background: #f00;, press Ctrl+S to save the change, go back to the experimental.html file, and click on the Firefox Preview tab at the bottom of the window. You should see a red screen, which was white before our changes were made.
Finally, create a new blank line on line 6, so we can add our JavaScript file. In Snippets, double click Insert JS <script> external, and type scripts/experimental.js for the src value. Again, we'll make a quick check that the link is correct, open experimental.js, and on line 4 add alert('Connected');, press Ctrl+S to save the change, go back to experimental.html, and click on the Firefox Preview tab. You should see a popup dialog with the title JavaScript Application, and the text Connected. Click on OK.
Now remove the modifications made in
experimental.css and
experimental.js, because red backgrounds, and popup
alerts are definitely out of fashion.
Why Check the Links?
Because if they're wrong, the browser simply won't tell you. Try it and see; change scripts/experimental.js to scripts/missing.js. Now check out the Firefox Preview. No error messages or warnings to be seen. You won't see any in Firebug either. Same with Aptana Studio, even in debug mode.
So before you spend hours trying to understand why your styles or code aren't working, make sure they are linked and loaded correctly first.
Oh, by the way, change scripts/missing.js back to scripts/experimental.js. We don't want any missing links!
Our XHTML file should now look something like:
01 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
02 <html xmlns='http://www.w3.org/1999/xhtml'>
03 <head>
04 <meta http-equiv="Content-Type" content='text/html; charset=UTF-8' />
05 <link rel="stylesheet" href="styles/experimental.css" type="text/css" media="screen" charset="utf-8" />
06 <script src="scripts/experimental.js" type="text/javascript" charset="utf-8"></script>
07 <title>LUG Programming Course, Lesson 3</title>
08 </head>
09 <body>
10 </body>
11 </html>
Running the Debugger
Aptana Studio doesn't just give a helping hand writing our source code, it also provides a great
debugging environment with the help of Firefox and Firebug.
To debug our code, we need to create a debugging configuration. Select Run from the menu bar, then click on Debug.... This opens the Debug Configuration Wizard. Click on the Web browser option, then on the new configuration icon (top left). Change the name to Experimental, and in Start Action, check the Specific page option, click on Browse..., and select experimental.html. Finally, click on Apply, then Close.
Now we need some code to debug. Last week I mentioned that the following code doesn't always work for all strings:
function countVowels(str) {
return str.match(/[aeiou]/ig).length;
}
So now we can find out when it doesn't work, and why. First we'll need the function, then we'll call it with two examples, and send the result to Firebug's console using console.log():
4 function countVowels(str) {
5 return str.match(/[aeiou]/ig).length;
6 }
7
8 console.log('countVowels("Hello"): ' + countVowels("Hello"));
9 console.log(" countVowels('Krk'): " + countVowels('Krk'));
Type the above into experimental.js, which we'll test in the debugger. Select Run from the menu bar, then Debug..., and then select Experimental and click on the Debug button. You may be asked if you want to use the Debug prospective for your debugging session. As this is the most useful prospective, answer Yes.
Aptana Studio opens the Firefox browser in order to communicate with the Firebug add-on. Since our script causes an error, and, by default, debugging halts on errors, you should see that the debug prospective halts on line 5 of experimental.js in Firebug. The Aptana Studio console also produces the error information:
TypeError: str.match(/[aeiou]/gi) has no properties
at countVowels(String) (Experiments/htdocs/scripts/experimental.js:5)
at [Top-level script]() (Experiments/htdocs/scripts/experimental.js:9)
It also produces the Firebug console output of:
countVowels("Hello"): 2
Some Detective Work Required
The code failed, giving a TypeError on line 5, while executing the code on line 9. This was the second call to countVowels(), but the first call to countVowels() worked correctly, and gave the correct answer of 2.
The difference is that the second call passed a string which has no vowels –
Krk. So,
Croatian islands are to blame! No, it's more likely that
match() is giving unexpected results. Unfortunately, the statement on line 5 is too compact to give us further information. You can add a breakpoint at line 5, by double clicking at the extreme left of the line (in the grey side bar), which causes a small blue dot to appear, but even stepping into the code doesn't provide any more information. However, we can rewrite it in a simpler, if more verbose form:
4 function countVowels(str) {
5 var regExp = /[aeiou]/ig;
6 var matchResult = str.match(regExp);
7 return matchResult.length;
8 }
Stop the current debugging session by selecting Run from the menu bar, then click on Terminate. Change the countVowels() function to the above code, and start the debugger again by selecting Run from the menu bar, then click on Debug Last Launched, or more simply, press F11.
This time the debugger stops at line 7. But now we have some more information to look at. The TypeError now tells us that matchResult has no properties, and, looking in the Variables window, we can see that it is actually null. Moving the mouse over matchResult on line 6 of the code also shows this value.
So, although the
official documentation doesn't say as much, the
match() function actually returns an array of matching strings,
or null if no matches are found, rather than an empty array, as I had supposed. Nice to know, sigh.
Since there are no vowels in Krk, it returns null. As null is not an array, it doesn't have a length property (in fact null has no properties at all).
Now we need to find a solution which fixes this problem. We've debugged the code, now we're bug fixing.
We can use the ternary operator to specifically test for null, in which case we'll return 0. Our final (and hopefully bug free) function now becomes:
4 function countVowels(str) {
5 var result = str.match(/[aeiou]/ig);
6 return (result) ? result.length : 0;
7 }
Stop the current debugging session by selecting Run from the menu bar, then click on Terminate. Change the countVowels() function to the above code, and start the debugger again by selecting Run from the menu bar, then click on Debug Last Launched, or more simply, press F11.
Finally, our program runs to completion, with the expected and correct console log of:
countVowels("Hello"): 2
countVowels('Krk'): 0
Other JavaScript Types
We've already talked about numbers, strings and boolean types. Now we'll take a look at arrays, associative arrays, regular expressions, and functions. Yes, I did write functions, they're a JavaScript type too.
Arrays
An
array is a list of values that can be accessed by a zero based index. Let's have a look at an example, using the Firebug console:
var array = [ 'Centigrade', 'Fahrenheit', 'Kelvin' ];
The first element of the array has the index value of 0, the second 1, and so on. The length of the array is given by the length property. Assigning a value using an index greater than or equal to the length causes the array to be increased in size, and undefined elements will have the value of undefined:
array[5] = 'Tulips';
array
gives:
["Centigrade", "Fahrenheit", "Kelvin", undefined, undefined, "Tulips"]
We can access the second element of the array with:
array[1]
which gives:
"Fahrenheit"
We can find the array length with:
array.length
which gives:
6
Specifying invalid index values (< 0, or >= length), gives undefined, so:
array[10] === undefined
results in:
true
Most importantly, we can use a 'classic'
for loop to iterate over our array:
for (var i = 0, l = array.length; i < l; i++) {
/* do something useful with */ array[i];
}
The 'classic'
for loop consists of three sections, separated by semicolons (
;), the initialising statement (
var i = 0, l = array.length), the conditional expression (
i < l), and the final statement (
i++). Yes, you can create multiple variables, separated by
commas (
,), and yes, you can
increment a variable by one using
++.
The statements inside the for code block are executed until the conditional expression is true. This could happen immediately, in which case the code block is not executed at all, such as when the array length is 0.
Arrays also have a few
methods, the most important of which are
push() and
join(). The
push(value) method adds an element to the end of the array, which saves you having to type something like
array[array.length] = value;. The
join(separator) method converts an array into a string, with each element separated by the
separator parameter string. The
join() method uses '
,' as the default, so if you want no separator at all use the empty string (
'').
Associative Arrays
Associative arrays are collections of name/value pairs. They are sometimes also known as dictionaries, hashes, lookup tables, or maps. JavaScript has no special type for associative arrays, rather it uses the
object type. In other words the JavaScript
Object is also an associative array. Let's look at an example, still using the Firebug console:
var map = { 'Fahrenheit': 32, 'Centigrade': 0, 'Kelvin': 273.15 };
map
which gives:
Object Fahrenheit=32 Centigrade=0 Kelvin=273.15
A specific value can be accessed using the corresponding name, thus:
map['Kelvin']
gives:
273.15
If the name is also a valid JavaScript identifier, and does not correspond to a keyword, then
dot notation can be used:
map.Human = 'Freezing cold!';
map.Human
gives:
"Freezing cold!"
Accessing a value using a name that does not exist in the associative array returns undefined, thus:
map['Polar Bear'] === undefined
results in:
true
Associated arrays use a special form for the
for loop known as
for...in. Associated array iteration looks like the following:
for (var key in map) {
/* do something useful with */ map[key];
}
Regular Expressions
One of the most repetitive jobs in programming is string manipulation, parsing, and validation. It is such a repetitive operation that most modern languages provide some form of
regular expression implementation, which provides a means of analysing strings, using a concise grammar.
In the JavaScript world we're looking at, regular expressions are frequently used to check the validity of form input fields, such as
email addresses. Unfortunately, the grammar of regular expressions is (or can be) somewhat complex. We used a
regular expression to find all single occurrences of vowels in a string:
var re = /[aeiou]/ig; // same as var = new RegExp('[aeiou]', 'ig');
The Prototype library, which we'll look at next week, also makes use of regular expressions to strip leading and trailing whitespace from strings (slightly reorganised here for clarity):
function strip(str) {
return str.replace(/^\s+/, '').replace(/\s+$/, '');
}
strip("\n \t \u00a0Goodbye Cruel World\n \t \u00a0")
gives:
"Goodbye Cruel World"
The first regular expression (/^\s+/) matches leading whitespace, and the second (/\s+$/) matches trailing whitespace. Curiously, I have found that the two replace() method calls can be substituted by one call – with a slightly altered regular expression:
function strip(str) {
return str.replace(/^\s+|\s+$/g, '');
}
strip("\n \t \u00a0Goodbye Cruel World\n \t \u00a0")
also gives:
"Goodbye Cruel World"
I have not seen this solution used in the wild (particularly, it is not used in Prototype).
Ruby's squeeze() method, which compacts all whitespace sequences to a single space (\u0020), could also be implemented using a regular expression, for example, in the Firebug console:
function squeeze(str) {
return str.replace(/\s+/g, ' ');
}
squeeze("Goodbye\n \t \u00a0Cruel\n \t \u00a0World")
gives:
"Goodbye Cruel World"
The easiest way to create a regular expression is to use a nearly equivalent
existing expression, and make modifications, using example text in a
regular expression tester.
Functions
Though we've become quite familiar with functions by now, there are still a few interesting points to note. A
function is a JavaScript type. It is an
Object and has properties. This is vitally important when extending other
Objects, as we'll see a little later on.
The most important property of a function is its
arguments. Actually, to be more correct,
arguments are a local variable of the function during its execution. Up to now, we have specifically defined our function parameters by name.
Most modern programming languages are able to provide optional parameters, or variable length argument lists. JavaScript achieves this by providing the arguments array. An example will help a lot here. Let's create a function which sums a series of numbers. That's any quantity of numbers, including zero:
function sum() {
var total = 0;
for (var i = 0, l = arguments.length; i < l; i++) {
total += arguments[i];
}
return total;
}
There are two things to note here; firstly we don't specify any parameters at all, and secondly, we iterate the arguments array just as for any other array. Let's try it out in the Firebug console. Type:
sum()
gives:
0
Whereas
sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
gives
55
But what about the opposite situation, where fewer arguments are given than the number of parameters required? Each unspecified argument becomes an
undefined parameter. Now, we could test for each parameter being undefined, but there is a quicker way, which takes advantage of two facts; to JavaScript
undefined is
false, and secondly that conditional logical or operators (
||) will short circuit if the left hand side is
true. Let's take a look at an example:
function quote(obj, open, close) {
obj = obj || '';
open = open || '"';
close = close || '"';
return open + obj + close;
}
Again, we'll test this little function in the Firebug console. Type:
quote()
gives:
""""
Whereas
quote(123.23)
gives
""123.23""
Finally:
quote(123.23, '(', ')')
gives
"(123.23)"
Just one small observation to make here. The empty string ('') is also false, so the function will always supply the default open and close quotes, even if you specified:
quotes('hello', '', '')
The classic developer's response? “That's not a bug, that's a feature!”. But it's perhaps arguable that this is a feature that needs correcting.
I said previously that a function is a JavaScript type. Type this in the Firebug console:
typeof quotes === 'function'
which gives:
true
Strange as it may seem, a
function type also has methods. There is one interesting method called
apply(). In our
sum() function above, we passed the values as individual parameters. But what if we already have an array of numbers to be summed? We can do this:
var numbers = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ];
sum.apply(this, numbers)
which gives:
55
The Power of JavaScript
JavaScript's Object is the real power of the language, together with the fact that everything we've seen up to now, numbers, strings, variables, and functions, are all Objects too. So let's move on to object oriented programming in JavaScript.
Up to now, all of our examples have been
procedural in nature. But this was just a simplification, because in reality JavaScript is an
object oriented programming language.
We previously used the keyword
this to find our defined variables and functions in Firebug. For all the work we've done up to now,
this effectively corresponds to the global object of the JavaScript environment. Every variable we created became a property of the global object, and every function became a method of the global object. Inside the browser, the global object is
window.
Let's put together a small object oriented example, which we'll store in our experimental.js file:
15 String.prototype.squash = function () {
16 return this.replace(/^\s+|\s+$/g, '').replace(/\s+/g, ' ');
17 };
18
This first short example actually adds a method squash to an existing built in type, String. There are two things to note here. Firstly, we are assigning a function definition to a property name, so we have to use the assignment syntax, name = value;. This is possible because a function in JavaScript is a type, like a string or number. In fact, we are adding a property, called squash to the prototype of String. So just as we have existing methods such as String.replace(), we also now have String.squash(). Secondly, in the function body we refer to the string object itself using the this keyword, accessing the replace() method of that specific string.
19 var Validator = {
20
21 // Returns the string, or null if not numeric
22 numeric: function (str) {
23 str = str.squash();
24 var result = (/^\d+$/i).exec(str);
25 return (result) ? result[0] : null;
26 },
27
28 // Returns the string, or null if not a valid email
29 email: function (str) {
30 str = str.squash();
31 var result = (/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$/i).exec(str);
32 return (result) ? result[0] : null;
33 }
34 };
35
In this second example there are also a few things to note. Firstly, we are using the associative array syntax to create our object:
var Validator = { /* ... */ };
Secondly, we have altered our function definition syntax slightly, in order to stay in line with the name: value, ... syntax of an associative array:
email: function (str) { /* ... / } // instead of function email(str) { / ... */ }
Thirdly, each function is separated by commas, as required by the associative array syntax. Finally the associative array is terminated with a semicolon (;).
The two validator methods numeric() and email(), both make calls to our string processing method, squash().
Next we'll add code to test our new methods:
36 console.log("Squash '\\n\\t\\u00a0Goodbye \\t\\u00a0 World\\n\\t\\u00a0': '" + "\n\t\u00a0Goodbye \t\u00a0 World\n\t\u00a0".squash() + "'");
37 console.log(" Numeric ' 000123 ': '" + Validator.numeric(' 000123 ') + "'");
38 console.log(" Email ' mickey_and_minny@mouse.co.uk ': '" + Validator.email(' mickey_and_minny@mouse.co.uk ') + "'");
Which produce the following results:
Squash '\n\t\u00a0Goodbye \t\u00a0 World\n\t\u00a0': 'Goodbye World'
Numeric ' 000123 ': '000123'
Email ' mickey_and_minny@mouse.co.uk ': 'mickey_and_minny@mouse.co.uk'
Our
Validator object in reality is a
singleton pattern with
static methods. Time to create a real multiple instantiation object.
Up to now, we have been relying on Firebug, and its console object to display the results of using our functions. That's fine if we're only going to test Firefox, but what about the other browsers? They may not have a debugger like Firebug, or even a console object. We can create a simple logger object for ourselves, which we'll do in a minute, first though we'll change all occurrences of console.log() to logger.log() in our experimental.js file – there are five of them. Next we'll add a <script> tag to our experimental.html page on line 6, and a <div> container tag on line 11, thus:
05 <link rel="stylesheet" href="styles/experimental.css" type="text/css" media="screen" charset="utf-8" />
06 <script src="scripts/debugging.js" type="text/javascript" charset="utf-8"></script>
07 <script src="scripts/experimental.js" type="text/javascript" charset="utf-8"></script>
08 <title>LUG Programming Course, Lesson 3</title>
09 </head>
10 <body>
11 <div id="log-output"></div>
12 </body>
Finally, in the public/scripts/ folder we'll create our debugging.js file, which will contain the following code:
01 /*
02 * Debugging utilities
03 */
04 function Logger() {
05 this._console = null;
06 if (window.console && window.console.log) {
07 this._console = window.console;
08 }
09 this.messages = [];
10 }
11
12 Logger.prototype = {
13 log: function (msg) {
14 msg = msg || '';
15 this.messages.push(msg);
16 if (this._console) {
17 this._console.log(msg);
18 }
19 },
20
21 show: function (id) {
22 if (id && (id = document.getElementById(id))) {
23 id.innerHTML = "<pre>" + this.messages.join('\n') + "</pre>";
24 }
25 else if (!this._console) {
26 alert(this.messages.join('\n'));
27 }
28 }
29 };
30
31 var logger = new Logger();
Our
Logger class is divided into two sections. Lines 4 to 10 contains the
constructor method. As its name implies, this is the method called to construct an instance of the class, and line 31 shows an example.
Note that, by convention, constructor methods start with an uppercase letter. Also note that we create instance variables using the this keyword, without using the var keyword (line 5). Unfortunately, that can be the cause of serious problems because if you misspell the name (this._cosole) you'll simply create a new property – you won't see a 'missing property' runtime error. You have been warned.
The second section, lines 12 to 29 defines the
prototype of the class. Here we have two methods,
log() which works in a similar fashion to Firebug's
console.log(), and
show() which displays all the logged messages.
Clearly, it would be a shame to stop using Firebug's console object when it's available. Lines 5 to 8 check if it is indeed available, in which case it keeps a local reference. Remember that window is the global object, and that we can check properties which are methods without invoking the method. So line 6 reads: “if window has a console property, and window.console has a log property then...”. It's not foolproof, we might find some browser with a console object and log property which is not a function, but that seems pretty improbable.
Our log method, lines 13 to 19, takes a message (defaulting to the empty string if none is provided), adds it to the internal array of messages, and passes it on to the console.log method, if one exists.
The show method, lines 21 to 28, displays all the messages logged up to now. It does this in three different ways; firstly (line 22) if you supply an identifier to an HTML tag, it will dynamically add the messages as HTML markup to that tag, secondly if you don't supply an identifier and there is no console.log (line 25), it will show the messages in a JavaScript alert dialog. The third option does nothing (if you don't supply an identifier and console.log exists) because the messages will already have been sent there.
To add HTML markup dynamically, we use a method
document.getElementById(), and a property
element.innerHTML. The method gets the element with the specified identifier, and the property substitutes all the elements' child nodes with the given markup.
Since I prefer to avoid alert dialogs, we will trigger the call to the logger.show() method by adding it to the onload event in the <body> tag, on line 10 of our experimental.html file:
10 <body onload="logger.show('log-output');">
11 <div id="log-output"></div>
12 </body>
The onload attribute tells the browser to evaluate the attribute value, when the document is loaded. In this case it will call our logger.show() method, which will change the contents of the tag with identifier equal to 'log-output', which just happens to be on line 11.
Reloading our experimental.html page should now show:
countVowels("Hello"): 2
countVowels('Krk'): 0
Squash '\n\t\u00a0Goodbye \t\u00a0 World\n\t\u00a0': 'Goodbye World'
Numeric ' 000123 ': '000123'
Email ' mickey_and_minny@mouse.co.uk ': 'mickey_and_minny@mouse.co.uk'
Although we could have created our Logger class as a singleton, as we did with validator, there may be situations where you need more than one. Perhaps one per JavaScript file, or one for your code, and one for someone else's library code. That way you can pick and choose the information to display, without getting swamped in an avalanche of messages.
Source Files
All the source files for this lesson, including the Aptana project file can be found in the
LUGPC3.zip archived file, distributed under the
GNU Lesser General Public License.
What's Next?
Next we'll take a look at the Prototype and script.aculo.us libraries, and how to dynamically modify a web page in more elegant ways.