Article

Script Smarter: Quality JavaScript from Scratch

Page: 1 2 3 4 5 6 7 8 Next

Getting Multiple Scripts to Work on the Same Page

When multiple scripts don't work together, it's almost always because the scripts want to assign event handlers for the same event on a given element. Since each element can have only one handler for each event, the scripts override one another's event handlers.

Solution

The usual suspect is the window object's load event handler, because only one script on a page can use this event; if two or more scripts are using it, the last one will override those that came before it.

We could call multiple functions from inside a single load handler, like this:

window.onload = function()  
{  
 firstFunction();  
 secondFunction();  
}

But, if we used this code, we'd be tied to a single piece of code from which we'd have to do everything we needed to at load time. A better solution would provide a means of adding load event handlers that don't conflict with other handlers.

When the following single function is called, it will allow us to assign any number of load event handlers, without any of them conflicting:

Example 1.7. add-load-listener.js  
 
function addLoadListener(fn)  
{  
 if (typeof window.addEventListener != 'undefined')  
 {  
   window.addEventListener('load', fn, false);  
 }  
 else if (typeof document.addEventListener != 'undefined')  
 {  
   document.addEventListener('load', fn, false);  
 else if (typeof window.attachEvent != 'undefined')  
 {  
   window.attachEvent('onload', fn);  
 }  
 else  
 {  
   var oldfn = window.onload;  
   if (typeof window.onload != 'function')  
   {  
     window.onload = fn;  
   }  
   else  
   {  
     window.onload = function()  
     {  
       oldfn();  
       fn();  
     };  
   }  
 }  
}

Once this function is in place, we can use it any number of times:

addLoadListener(firstFunction);  
addLoadListener(secondFunction);  
addLoadListener(twentyThirdFunction);

You get the idea!

Discussion

JavaScript includes methods for adding (and removing) event listeners, which operate much like event handlers, but allow multiple listeners to subscribe to a single event on an element. Unfortunately, the syntax for event listeners is completely different in Internet Explorer than it is in other browsers: where IE uses a proprietary method, others implement the W3C Standard. We'll come across this dichotomy frequently, and we'll discuss it in detail in Chapter 13, Basic Dynamic HTML.

The W3C standard method is called addEventListener:

window.addEventListener('load', firstFunction, false);

The IE method is called attachEvent:

window.attachEvent('onload', firstFunction);

As you can see, the standard construct takes the name of the event (without the "on" prefix), followed by the function that's to be called when the event occurs, and an argument that controls event bubbling (see Chapter 13, Basic Dynamic HTML for more details on this). The IE method takes the event handler name (including the "on" prefix), followed by the name of the function.

To put these together, we need to add some tests to check for the existence of each method before we try to use it. We can do this using the JavaScript operator typeof, which identifies different types of data (as "string", "number", "boolean", "object", "array", "function", or "undefined"). A method that doesn't exist will return "undefined".

if (typeof window.addEventListener != 'undefined')  
{  
 ... window.addEventListener is supported  
}

There's one additional complication: in Opera, the load event that can trigger multiple event listeners comes from the document object, not the window. But we can't just use document because that doesn't work in older Mozilla browsers (such as Netscape 6). To plot a route through these quirks we need to test for window.addEventListener, then document.addEventListener, then window.attachEvent, in that order.

Finally, for browsers that don't support any of those methods (Mac IE 5, in practice), the fallback solution is to chain multiple old-style event handlers together so they'll get called in turn when the event occurs. We do this by dynamically constructing a new event handler that calls any existing handler before it calls the newly-assigned handler when the event occurs. (This technique was pioneered by Simon Willison.)

Example 1.8. add-load-listener.js (excerpt)  
 
var oldfn = window.onload;  
if (typeof window.onload != 'function')  
{  
 window.onload = fn;  
}  
else  
{  
 window.onload = function()  
 {  
   oldfn();  
   fn();  
 };  
}

Don't worry if you don't understand the specifics of how this works -- we'll explore the techniques involved in much greater detail in Chapter 13, Basic Dynamic HTML. There, we'll learn that event listeners are useful not just for the load event, but for any kind of event-driven script.

Hiding JavaScript Source Code

If you've ever created something that you're proud of, you'll understand the desire to protect your intellectual property. But JavaScript on the Web is an open-source language by nature; it comes to the browser in its source form, so if the browser can run it, a person can read it.

There are a few applications on the Web that claim to offer source-code encryption, but in reality, there's nothing you can do to encrypt source-code that another coder couldn't decrypt in seconds. In fact, some of these programs actually cause problems: they often reformat code in such a way as to make it slower, less efficient, or just plain broken. My advice? Stay away from them like the plague.

But still, the desire to hide code remains. There is something that you can do to obfuscate, if not outright encrypt, the code that your users can see.

Solution

Code that has been stripped of all comments and unnecessary whitespace is very difficult to read, and as you might expect, extracting individual bits of functionality from such code is extremely difficult. The simple technique of compressing your scripts in this way can put-off all but the most determined hacker. For example, take this code:

Example 1.9. obfuscate-code.js (excerpt)  
 
var oldfn = window.onload;  
if (typeof window.onload != 'function')  
{  
 window.onload = fn;  
}  
else  
{  
 window.onload = function()  
 {  
   oldfn();  
   fn();  
 };  
}

We can compress that code into the following two lines simply by removing unnecessary whitespace:

Example 1.10. obfuscate-code.js (excerpt)  
 
var oldfn=window.onload;if(typeof window.onload!='function'){  
window.onload=fn;}else{window.onload=function(){oldfn();fn();};}

However, remember that important word -- unnecessary. Some whitespace is essential, such as the single spaces after var and typeof.

Discussion

This practice has advantages quite apart from the benefits of obfuscation. Scripts that are stripped of comments and unnecessary whitespace are smaller; therefore, they're faster loading, and may process more quickly.

But please do remember that the code must remain strictly formatted using semicolon line terminators and braces (as we discussed in the section called "Using Braces and Semicolons (Consistent Coding Practice)"); otherwise, the removal of line breaks will make lines of code run together, and ultimately cause errors.

Before you start compression, remember to make a copy of the script. I know it seems obvious, but I've made this mistake plenty of times, and it's all the more galling for being so elementary! What I do these days is write and maintain scripts in their fully spaced and commented form, then run them through a bunch of search/replace expressions just before they're published. Usually, I keep two copies of a script, named myscript.js and myscript-commented.js, or something similar.

We'll come back to this subject in Chapter 20, Keeping up the Pace, where we'll discuss this among a range of techniques for improving the speed and efficiency of scripts, as well as reducing the amount of physical space they require.

Debugging a Script

Debugging is the process of finding and (hopefully) fixing bugs. Most browsers have some kind of bug reporting built in, and a couple of external debuggers are also worth investigating.

Understanding a Browser's Built-in Error Reporting

Opera, Mozilla browsers (such as Firefox), and Internet Explorer all have decent bug reporting functionality built in, but Opera and Mozilla's debugging tools are the most useful.

Opera
Open the JavaScript console from Tools > Advanced > JavaScript console. You can also set it to open automatically when an error occurs by going to Tools > Preferences > Advanced > Content, then clicking the JavaScript options button to open its dialog, and checking Open JavaScript console on error.

Firefox and other Mozilla browsers
Open the JavaScript console from Tools > JavaScript console.

Internet Explorer for Windows
Go to Tools > Internet Options > Advanced and uncheck the option Disable script debugging, then check the option Display a notification about every script error, to make a dialog pop up whenever an error occurs.

Internet Explorer for Mac
Go to Explorer > Preferences > Web Browser > Web Content and check the Show scripting error alerts option.

Safari doesn't include bug reporting by default, but recent versions have a "secret" Debug menu, including a JavaScript console, which you can enable by entering the following Terminal command. (The $ represents the command prompt, and is not to be typed.)

$ defaults write com.apple.safari IncludeDebugMenu -bool true

You can also use an extension called Safari Enhancer, which includes an option to dump JavaScript messages to the Mac OS Console; however, these messages are not very helpful.

Understanding the various browsers' console messages can take a little practice, because each browser gives such different information. Here's an example of an error -- a mistyped function call:

function saySomething(message)  
{  
 ...  
 alert(message);  
}  
saySometing('Hello world');

Firefox gives a concise but very accurate report, which includes the line number at which the error occurred, and a description, as shown in Figure 1.1, "The JavaScript errors console in Firefox".

Firefox's JavaScript Console
Figure 1.1. The JavaScript errors console in Firefox

As Figure 1.2, "The JavaScript console in Opera" illustrates, Opera gives an extremely verbose report, including a backtrace to the event from which the error originated, a notification of the line where it occurred, and a description.

A backtrace helps when an error occurs in code that was originally called by other code; for example, where an event-handler calls a function that goes on to call a second function, and it's at this point that the error occurs. Opera's console will trace this process back through each stage to its originating event or call.

Internet Explorer gives the fairly basic kind of report shown in Figure 1.3, "The JavaScript console in Windows IE". It provides the number of the line at which the interpreter encountered the error (this may or may not be close to the true location of the actual problem), plus a summary of the error type, though it doesn't explain the specifics of the error itself. (Internet Explorer is particularly bad at locating errors in external JavaScript files. Often, the line number it will report as the error location will actually be the number of the line at which the script is loaded in the HTML file.)

Opera's JavaScript Console
Figure 1.2. The JavaScript console in Opera

IE's JavaScript Console
Figure 1.3. The JavaScript console in Windows IE

As you probably gathered, I'm not overly impressed by Internet Explorer's error reporting, but it is vastly better than nothing: at least you know that an error has occurred.

Using alert

The alert function is a very useful means of analyzing errors -- you can use it at any point in a script to probe objects and variables to see if they contain the data you expect. For example, if you have a function that has several conditional branches, you can add an alert within each condition to find out which is being executed:

Example 1.11. debugging-dialogs.js  
 
function checkAge(years)  
{  
 if (years < 13)  
 {  
   alert('less than 13');  
 
   ... other scripting  
 }  
 else if (years >= 13 && years <= 21)  
 {  
   alert('13 to 21');  
 
   ... other scripting  
 }  
 else  
 {  
   alert('older');  
 
   ... other scripting  
 }  
}

Maybe the value for years is not coming back as a number, like it should. You could add to the start of your script an alert that tests the variable to see what type it is:

function checkAge(years)  
{  
 alert(typeof years);  
 ...

In theory, you can put any amount of information in an alert dialog, although a very long string of data could create such a wide dialog that some of the information would be clipped or outside the window. You can avoid this by formatting the output with escape characters, such as \n for a line break.

Using try-catch

The try-catch construct is an incredibly useful way to get a script just to "try something," leaving you to handle any errors that may result. The basic construct looks like this:

Example 1.12. debugging-trycatch.js (excerpt)  
 
try  
{  
 ... some code  
}  
catch (err)  
{  
 ... this gets run if the try{} block results in an error  
}

If you're not sure where an error's coming from, you can wrap a try-catch around a very large block of code to trap the general failure, then tighten it around progressively smaller chunks of code within that block. For example, you could wrap a try brace around the first half of a function (at a convenient point in the code), then around the second half, to see where the error occurs; you could then divide the suspect half again, at a convenient point, and keep going until you've isolated the problematic line.

catch has a single argument (I've called it err in this case), which receives the error object; we can query properties of that object, such as name and message, to get details about the error.

Often, I use a for-in iterator to run through the entire object and find out what it says:

Example 1.13. debugging-trycatch.js (excerpt)  
 
for (var i in err)  
{  
 alert(i + ': ' + err[i]);  
}

Writing to the Page or Window

If you're examining a great deal of data while debugging, or you're dealing with data that's formatted in a complicated way, it's often better to write that data directly to a page or popup window than to try to deal with lots of alert dialogs. If you're examining data in a loop, in particular, you could end up generating hundreds of dialogs, each of which you'll have to dismiss manually?a very tedious process.

In these kinds of situations, we can use an element's innerHTML property to write the data to the page. Here's an example in which we build a list using the contents of an array (data), then write it into a test div:

Example 1.14. debugging-writing.js (excerpt)  
 
var test = document.getElementById('testdiv');  
 
test.innerHTML += '<ul>';  
for (var i = 0; i < data.length; i++)  
{  
 test.innerHTML += '<li>' + i + '=' + data[i] + '</li>';  
}  
test.innerHTML += '</ul>';

We can also write the data into a popup, which is useful if there's no convenient place to put it on the page:

Example 1.15. debugging-writing.js (excerpt)  
 
var win = window.open('', win, 'width=320,height=240');  
 
win.document.open();  
win.document.write('<ul>');  
for (var i = 0; i < data.length; i++)  
{  
 win.document.write('<li>' + i + '=' + data[i] + '</li>')  
}  
win.document.write('</ul>');  
win.document.close();

You can format the output however you like, and use it to structure data in any way that makes it easier for you to find the error.

When you're working with smaller amounts of data, you can gain a similar advantage by writing the data to the main title element:

Example 1.16. debugging-writing.js (excerpt)  
 
document.title = '0 = ' + data[0];

This final approach is most useful when tracking data that changes continually or rapidly, such as a value being processed by a setInterval function (an asynchronous timer we'll meet properly in Chapter 14, Time and Motion).

Using an External Debugger

I can recommend two debuggers:

  • Venkman for Mozilla and Firefox
  • Microsoft Script Debugger for Windows Internet Explorer

External debuggers are a far more detailed way to analyze your scripts, and have much greater capabilities than their in-browser counterparts. External debuggers can do things like stopping the execution of the script at specific points, or watching particular properties so that you're informed of any change to them, however it may be caused. They also include features that allow you "step through" code line by line, in order help find errors that may occur only briefly, or are otherwise difficult to isolate.

External debuggers are complex pieces of software, and it can take time for developers to learn how to use them properly. They can be very useful for highlighting logical errors, and valuable as learning tools in their own right, but they're limited in their ability to help with browser incompatibilities: they're only useful there if the bug you're looking for is in the browser that the debugger supports!

Strict Warnings

If you open the JavaScript console in Firefox you'll see that it includes options to show Errors and Warnings. Warnings notify you of code that, though it is not erroneous per se, does rely on automatic error handling, uses deprecated syntax, or is in some other way untrue to the ECMAScript specification. (To see these warnings, it may be necessary to enable strict reporting by typing in the address about:config and setting javascript.options.strict to true.)

For example, the variable fruit is defined twice in the code below:

Example 1.17. strict-warnings.js (excerpt)  
 
var fruit = 'mango';  
 
if (basket.indexOf('apple') != -1)  
{  
 var fruit = 'apple';  
}

We should have omitted the second var, because var is used to declare a variable for the first time, which we've already done. Figure 1.4, "The JavaScript warnings console in Firefox" shows how the JavaScript console will highlight our error as a warning.

Firefox's JavaScript Warnings Console
Figure 1.4. The JavaScript warnings console in Firefox

There are several coding missteps that can cause warnings like this. For example:

  • re-declaring a variable - This produces the warning, "redeclaration of var name," as we just saw.
  • failing to declare a variable in the first place - This oversight produces the warning, "assignment to undeclared variable name." This might arise, for example, if the first line of our code read simply fruit = 'mango';
  • assuming the existence of an object - This assumption produces the warning "reference to undefined property name."

For example, a test condition like if (document.getElementById) assumes the existence of the getElementById method, and banks on the fact that JavaScript's automatic error-handling capabilities will convert a nonexistent method to false in browsers in which this method doesn't exist. To achieve the same end without seeing a warning, we would be more specific, using if(typeof document.getElementById != 'undefined').

There are also some function-related warnings, and a range of other miscellaneous warnings that includes my personal favorite, "useless expression," which is produced by a statement within a function that does nothing:

Example 1.18. strict-warnings.js (excerpt)  
 
function getBasket()  
{  
 var fruit = 'pomegranate';  
 fruit;  
}

For a thorough rundown on the topic, I recommend Alex Vincent's article Tackling JavaScript strict warnings.

Warnings don't matter in the sense that they don't prevent our scripts from working, but working to avoid warnings helps us to adopt better coding practice, which ultimately creates efficiency benefits. For instance, scripts run faster in Mozilla if there are no strict warnings, a subject we'll look at again in Chapter 20, Keeping up the Pace.

Type Conversion Testing

Although we shouldn't rely on type conversion to test a value that might be undefined, it's perfectly fine to do so for a value that might be null, because the ECMAScript specification requires that null evaluates to false. So, for example, having already established the existence of getElementById using the typeof operator as shown above, it's perfectly safe from then on to test for individual elements as shown below, because getElementById returns null for nonexistent elements in the DOM:

if (document.getElementById('something'))  
{  
 ... the element exists  
}

Summary

In this chapter, we've talked about best-practice approaches to scripting that will make our code easier to read and manage, and will allow it to degrade gracefully in unsupported devices. We've also begun to introduce some of the techniques we'll need to build useful scripts, including the ubiquitous load event listener that we'll use for almost every solution in this book!

We've covered some pretty advanced stuff already, so don't worry if some of it was difficult to take in. We'll be coming back to all the concepts and techniques we've introduced here as we progress through the remaining chapters.

If you liked this article, share the love:
Print-Friendly Version Suggest an Article

Sponsored Links