Writing Effective Javascript Code (or How I Learned to Stop Worrying and Love the Unit Test)

Like most programmers I feel there are always ways of improve my coding skills. Whether it’s the perennial ‘write more documentation’, gain a better understanding of my current programming language or even just adopt a good programming style. Recently our company had a staff development week and I took this opportunity to pursue some of these interests. My main focus was on Unit Testing and after a week spent delving into this world I emerged with a new found enthusiasm for testing techniques, better confidence in my own skills (thanks to the tests I wrote) and a desire to spread the word.

The following entry is from a presentation I gave to my fellow ILRT programmers which covers some of the javascript tips and tricks I discovered on the web to help me (and others) become a better programmer. I conclude with some information on testing javascript applications using JSUnit and Selenium Core.

(I appologise for the layout but after several hours spent fighting with WordPress I’ve given up nicely trying to format everything)

Good Coding Practice

Browser detection

Avoid using any user-agent-like comparison mechanisms. Instead write code to detect objects as needed.

So instead of


function getObjectHeight = function(obj) { 
    if (navigator.appName == "Netscape") return obj.offsetHeight;
    else if (navigator.appName == "Microsoft Internet Explorer") return obj.clientHeight;
}

Use object detection


function getObjectHeight = function(obj) { 
    return (obj.offsetHeight) ? obj.offsetHeight : (obj.clientHeight) ? obj.clientHeight : obj.style.height; 
}

Prototyping

The Javascript prototype functionality is a highly useful mechanism allowing you to modify JS object properties & methods directly.

This is very powerful and therefore very dangerous (you can overwrite and remove existing properties with ease). When modifying JS objects, always check they exist first. Prototyping is mostly used to implement features missing in Javascript.


if (!String.prototype.startsWith) { 
    String.prototype.startsWith = function(s) {
        return this.indexOf(s) === 0;
    }
}

Together with object detection prototyping allows you to create browser-independent code with ease. On excellent framework built on this approach is Prototype.js

Prototyping also allows you to produce Java-like object classes.

function FormEditor() { 
    // constructor logic here ...

    // define class variables
    this.UniqueID = 0;

    // enumerations can be implemented
    Highlight =
	{
	 	BELOW : 0,
		RIGHT : 1,
		THIS : 2,
		COLUMN : 3,
		IMAGE : 4
	};

    //further functions called
    this.initPage();
}

FormEditor.prototype.initPage = function()
{
	this.UniqueID = 10;
}

// now create an instance of our new formeditor class
var myFormEditor = new FormEditor();

myFormEditor.UniqueID; // returns 10

Inheritance can also be implemented using the prototype functionality as the prototype is itself a property that can be assigned to a sub-class class. There are many different ways of doing this, but maybe the simplest is as follows:

Car.prototype = new Vehicle;

Minimise use of the global namespace

As will mention later this improves efficiency, however it is also general good coding practice – makes code clearer & less open to abuse/mistakes (such as accidentally overriding someone else’s code).


var ILRT.eels.cer = { 
    values : new Array();
    needToConfirm : true;
    confirmString : "Are you sure?";
} 
...
ILRT.eels.cer.confirmString // returns "Are you sure?"

== vs ===

Use the object equality operator (===) whenever possible

"10" == 10 // returns true
"10" === 10 // returns false

Testing Arguments

Avoid testing null – it tells us what your not looking for rather then what you are looking for.


if (x !== null) {
     // do something
}

Instead of the above, use typeof


if (typeof(x) === "string") {
    // do something
}

If you must test for null, use


if (x) {
    // do something
}

Check for specific type of object with instanceof


if (x instanceof Array) {
    // do something
}

Event Handling

Event handling can cause lots of problems (due to different browser implementations). Also standard ECMAScript only allows one function to be attached to a event. Therefore use a better event handling utility. I recommend the Yahoo! Events YUI.

YAHOO.util.Event.addListener(window, "load", function() { formeditor = new FormEditor() });

Tips for Improving Efficiency

Use local references whenever possible

I found this to be of more for IE benefit then FF, but generally doesn’t hurt and is better programming practice. Javascript sequentially searches up the stack chain for variable references therefore local variables will be found first. Speed up code by creating a local variable pointing to a global variable.

var thisDoc = document; var myobject = thisDoc.getElementById('myobjectid');

Cache frequently used variables & properties

As JS is an interpreted language so properties are dynamically evaluated on each request. Therefore you can speed up programs by locally caching the values of objects and then referring to the local copy wherever possible. This is particularly relevant in loops dependent on dynamic values.

So instead of


for(var i=0; i < myarray.length; i++) {
    // do some stuff
}

Use


for(var i=0, len = myarray.length; i < len; i++) {
    // do some stuff
}

Equally, cache often used element properties (as referencing them induces overhead)

So rather then repeatedly using statements such as


document.getElementById('myobjectid').style.borderWidth = '1px'; document.getElementById('myobjectid').style.fontSize = '90%';

It is better to do:


var s = document.getElementById('myobjectid').style;
s.borderWidth = '1px';
s.fontSize = '90%';

Minimise DOM insertions

As browsers will re-evaluate the page on each change to the dom, keep new element insertions to a minimum. When you do modify the dom, it’s best to lump all insertions together.

So rather then:


var A = document.createElement("div");
var B = document.createElement("div");
var C = document.createElement("div");
document.appendChild(A);
A.appendChild(B);
B.appendChild(C);

Write


var A = document.createElement("div");
var B = document.createElement("div");
var C = document.createElement("div");
A.appendChild(B);
B.appendChild(C);
document.appendChild(A);

As browsers will only update the page display if visible elements have been modified, another approach is to hide element, make insertions and the re-display element.


var A = document.createElement("div");
var B = document.createElement("div");
var C = document.createElement("div");
document.appendChild(A);
A.style.display = "none";
A.appendChild(B);
A.appendChild(C);
A.style.display = "block";

innerHTML vs createElement()

Jury seems to be out on whether to use innerHTML or document.createElement() to generate new dom content. In theory innerHTML has to interpret the string (much like eval does) which would indicate a large overhead. However if using IE, innerHTML is much faster. I didn’t notice any difference in FF. I prefer createElement() but for small code innerHTML is much more compact & human readable.


a.innerHMTL = "
";

vs


var tmpDiv = document.createElement("div");
tmpDiv.id = "newElementID";
a.appendChild(tmpDiv);

Optimising page load time

Smaller code = quicker download times. Lots of crunchers (whitespace removal) and compressors (code reduction) tools out there. Can also use gzip compression on stream transfer. I can recommend JSMin for code crunching. However while in development this is probably not recommended.

You can also combine js code into a single file as most browsers have a limit on the number of HTTP requests open at any given time, aka HTTP pipelining (approx ~ 8 connections). You can also spread connections over several different web servers – use one for static files and another for dynamic content. Again, best left till deployment.

Also…

  • Avoid with(), using try/catch inside loops and the dreaded eval() function.
  • Choose switch loops over multpleif..else if loops.

General Advice for Javascript Developers

  • Best practice to develop in Firefox first, then test in IE.
  • Can use Javascript Lint (not the similarly named JSLint) to scan js files, verify code and detect mistakes (duplicate variable assignment, missing semicolons, unreachable code, missing return values etc).
  • Can’t always trust browser-provided functionality (e.g. document.getElementById()). Useful to check out quirksmode.org.
  • Drip can detect IE memory leaks (untested?)
  • Write a build script for final deployment – this can compress code, combine into a single file and automatically test it before release.
  • Provide more user-friendly error messages by overriding onerror()

// attaching an onerror event handler to an image object
myImage.onerror = function(){ alert('image url not found') };

// attaching an error handler to page using YUI Event
YAHOO.util.Event.addListener(window, "error", myerrorhandler, this, true);

Debugging

  • Firefox: Use Firebug!!! Has excellent console debugger, dynamic property evaluation and profiling tools.
  • Internet Explorer: Use Microsoft Script Debugger (part of Office 2003+) and the debugger; keyword. Provides similar functionality to Firebug. It allows breakpoints and watch expressions.

Separation of concerns

Generally good idea to keep all styling issues to css and all code&event handling issues to javascript. Especially true as it is possible to embed javascript code into style sheets.

Rather then:

HTML

Javascript:

document.getElementById("examplediv").style.borderWidth = "2px;";

CSS:


# examplediv {
    width: expression(650 < document.body.clientWidth ? '650px' : '100%' );
}

Instead write:

HTML:

Javascript:


// use classes to change appearance, rather then with the style object 
document.getElementById("examplediv").className = "examplediv";
// add onclick function to example div
YAHOO.util.Event.addListener("examplediv", "click", runme);

// add a resize listener to the window object
YAHOO.util.Event.addListener(window, "resize", recalcWidth);

// dynamically calculate the new width of example div
function recalcWidth(event)
{
	document.getElementById("examplediv").style.width =
 		(650 < document.body.clientWidth ? '650px' : '100%';);
}

CSS:


.examplediv { border-width: 2px; }

Testing Javascript Code

UnitTesting

The defacto standard for Javascript is JSUnit. Works like most other unittesting mechanisms. I’ll start of with a simple jsunit test for the String.prototype.startsWith() function.


function setUp() {
    // code placed here is run before test
}

function testStartsWith()
{
    assertEquals('Test startsWith function exists', 'function', typeof (String.prototype.startsWith));
    assertFalse('Test null input',"String".startsWith(null));
    assertTrue('Test empty string input', "String".startsWith(""));
    assertTrue('Test it works as expected',"String".startsWith("String"));
    assertFalse('Test a false condition',"String".startsWith("Sstri"));
    assertFalse('Test case sensitivity',"String".startsWith("string"));
}

function tearDown() {
    // code placed here runs after test
}

Why use unit testing in general?

  • Find all sorts of bugs
  • Promotes good programming practice – it really makes you think about the code your writing.
    • What are the inputs?
    • What happens if I pass missing parameters?
    • How should I handle errors (in JS)?
    • Forces you to consider border scenarios
    • Makes you consider the design of your code (function grouping & organisation)
  • Allows you to rapidly check that new functionality has not effected existing code.
    • This lends itself well to team projects where you might not be able to accurately predict the impact your changes have on existing code. You can then run all existing unit tests before check-in to a repository.
  • Enhances existing practice – everyone tests their code. I reckon ~80% of the unit tests are ones you would have written/tested for anyway.

Why use JSUnit

  • All too easy to write a piece of code that can effect other code (modifications to the global namespace or changes to object prototypes)
  • Can easily run your tests in multiple browsers (see example below) and on different machines to check for incompatibilities
  • Helpful to check your code after you’ve used a cruncher/compressor

Real world example


function replaceSups() {
    // get all relevant hint labels
    var hints = getElementsByClass('xforms-hint',document,'label');

    // iterate over this array
    for(i = 0; i < hints.length; i++)
    {
        // replace matching [sups] with html 
        hints[i].innerHTML = hints[i].innerHTML.replace(/[sup]/g,"");
        hints[i].innerHTML = hints[i].innerHTML.replace(/[/sup]/g,"");
    }
}

function setUp()
{
    // create a dummy div, and add some child nodes
    var mydiv = document.createElement("div");
    mydiv.className = "xforms-hint";

    var mylabel1 = document.createElement("label");
    mylabel1.innerHTML = "Hello[sup]test[/sup]here";
    mylabel1.className = "xforms-hint";
    var mylabel2 = document.createElement("label");
    mylabel2.innerHTML = "Second[sup]test 2[/sup]here";

    mydiv.appendChild(mylabel1);
    mydiv.appendChild(mylabel2);

    documentInsertionPoint.appendChild(mydiv);
}

function testReplaceSupsMethod()
{
    // run the function under test
    replaceSups();

    assertEquals("Test function ignores non-matching classnames","Second[sup]test 2[/sup]here",mylabel2.innerHTML);

    // succeeds in Firefox
    // however same test fails in IE: "hellotesthere"

    assertEquals("Test function works as expected",
        "hellotesthere",
        mylabel1.innerHTML.toLowerCase());
}

Negatives of unit testing

  • Writing unit tests can become tedious

  • You can become too anal about your code

  • Can only test for foreseeable events

  • Can become too reliant on the tests!

  • Difficult (but not impossible) to test heavily dynamic or event driven functionality (aka Ajax)

Selenium Core & IDE

Selenium Core is a tool to test web applications. Runs directly in any javascript-enabled browser. Useful to emulate user interactions and verify dynamic page content.

Selenium IDE is a Firefox extension that acts as a selenium test macro recorder (you start it up, then interact with the browser directly and it records your actions which you can then playback).

Selenium CORE

Negatives:

  • Doesn’t handle loops
  • Has nosetUp() or tearDown() functionality.
  • Due to cross-site scripting issues you need to run the Selenium Core code on the website to be tested.

References and Useful links

YUI Theater Videos

Optimizing Javascript

Good Coding Practice

Testing

4 thoughts on “Writing Effective Javascript Code (or How I Learned to Stop Worrying and Love the Unit Test)

  1. Selenium does not natively allow for looping, but with Core, you can add a user extension for handling loops.

  2. Selenium RC will allow you to write your tests with most main stream programming languages as well as extending test frameworks such as junit for java.

Comments are closed.