News

Build Maintainable JavaScript

At the recent Rich Web Experience conference, it fell to Yahoo! Senior Front End Engineer Nicholas C. Zakas to tell developers how to make their code work tomorrow, and not just today.

Zakas started with general advice about code maintenance that most developers have heard, such as the need to comment hacks. Should comments not be added, some developer might try to "fix" those hacks later. Zakas then got down to the specifics of maintaining JavaScript (JS) code, which perked up the audience.

To start with, Zakas advocated respecting the boundaries between the three technologies used in JS-based programs, with behavior in JS, presentation in cascading style sheets (CSS) and structure in HTML.

In practical terms, this means starting by separating HTML and JS. Don't put JS in HTML like this:

<a  onclick="sayHi()">text</a>

And don't put HTML in JS either:

element.innerHTML = "<div>" 
  + "<a  href="/js/">text</a></div>";

Next, keep expressions written in JS out of CSS. Zakas related how in a previous job he found that a program was throwing a JS error. Yet even when he commented out all of the JS, the JS error still happened. It took him two days to find that the JS error was embedded in the CSS. Here is some JS in CSS:

div.hd {
  width: 
  expression(ref.offsetWidth+"px");
  }

And, controversial as it might seem, Zakas insisted that you have to keep your CSS out of your JS. If your JS is changing the styles of your elements dynamically, where are your styles coming from? Do you have to start debugging JS to find them? Wherever you stop and say "style," ask yourself whether this really needs to be done in JS.

Instead, Zakas advocated defining classes for everything on your page. That is definitely slower than using the style object. But the tradeoff is having a nice, loosely coupled interface. And, if you're changing styles in JS all of the time, it's going to be slow anyway.

Event handlers should handle events -- period. This is talking about the observer pattern. So don't do this:

function handleKeyPress(event){
  if (event.keyCode == 13){
  var value = 5 * 
  parseInt(txt.value);
  div.innerHTML =  value; 
  alert("Updated");
  }
  }

Instead do this:

function handleKeyPress(event){
  if (event.keyCode == 13){
  performCalculation();
  updateUI();
  }
  }

Using the second way means that you don't have to worry about pulling in information mistakenly from the event handler. Also, you can easily change event handlers later. You don't have to provide extra information, and you can do automated scripting of your Web object that can really help in testing.

Don't Modify Objects You Don't Own
If an object is not your responsibility, don't change what the methods do. Don't add methods and don't override them. Zakas described how at Yahoo! they were using the YUI libraries and were having some problems handling events. There's a method on Yahoo.util.event called stop.event, all in one handy function. That is, it's handy when it's doing what you expect it to do. However, it turned out one developer had redefined stop.event to do something else, inserting logic he'd needed to do his specific thing. It took two days to track it down. And it turned out it was someone who had left the team, leaving behind his unmaintainable code.

You don't own Object, Array, RegExp, String, Number, Boolean, Date and Function. Zakas said these all exist at such a core level in JS that any changes made here can really affect things and throw everyone off. That includes modifications to the prototypes of these base objects. Don't touch the prototype. If people start doing this, there will be a clash.

And avoid globals. All publicly accessible functions/variables should be attached to an object. Likewise, namespace your objects. You can keep a lot of like functionality together, but don't go overboard -- three levels are enough. .NET and Java have really long namespaces. Remember that JS imposes a slight performance penalty each time you hit a dot.

"Verbose code is nice up to a point…but not beyond," Zakas emphasized.

He said not to overuse popular techniques, such as closures and nested functions. Otherwise, it can be tough to follow the data as it's going through the code. Always see if you can avoid this to accomplish your task.

"We used to use closures for event handling to keep it in scope," Zakas admitted. "But many routines exist to do that now."

Another popular technique that Zakas said is overused is object literals.

"People think object literals are sexy," he said. But you should use them only to define objects once (singletons) because you'll never have to redefine the object.

He did agree that object literals are perfectly valid as a way to pass structure data back and forth, as long as you don't use them to define objects and methods. You should be defining a constructor instead. So Zakas wants you to cut back on your object literals.

"It will make your code much more readable and understandable."

Throw Your Own Errors
Zakas invited the audience to imagine themselves using their app when suddenly a box pops up saying, "Null is undefined." Huh? The lesson is, don't let the browser do your debugging for you. Make your errors pull their weight and actually tell you what's happening.

function add5(value) {
  if (arguments.length < 1) {
  throw new 
  Error("add5:  Not enough args");
  } 
  return value + 5; 
  }

In general, avoid null comparisons, he said. They aren't specific enough. Any time you're comparing something to null, ask yourself what is the value you're actually looking for that will cause this not to fail? And use instanceof for specific types of objects:

if (value instanceof
  constructor){

Use typeof to test for string, number, and Boolean:

if (typeof value == "string") {

Also, use constants. Sure, JS doesn't have constants in the strict sense, but Zakas was referring to local constants. He added that business logic in side functions is frail, so touch that as little as possible. He proposed using constants for repeated unique values, UI strings, URLs and any value that may change in the future.

That holds true especially with internationalization. Using constants cuts down on recoding greatly. Plus there are always people in marketing and interface design who want to change such values. Don't assume they'll stay put. Take them out of the body of the code and put them somewhere else where they won't change the business logic.

Go Old School and Compile
JS doesn't make you compile; it doesn't stop you from doing that either. Zakas noted that the compiler optimizes, and it can tell you when something's wrong. Likewise, JS doesn't make you use one class per file, so it's easy to just write one humongous JS file. Try instead arranging your objects to namespaces in folders containing a single object. Then, in your build process, combine files into one file and use it without worrying about the complexity. Compiling produces a single executable at the end.

And the build part can do all kinds of cool things. JS Lint looks at your code to find syntax errors before they get to the browser. And you can crunch your files -- remove white space, strip out comments. YUI Compressor does this. You get a smaller file and one that's sure to be syntactically correct.

Zakas' advice here boiled down to a couple of ideas:

  • Use one object or object definition per file, indicating dependencies; and
  • Use a build step that combines files in appropriate order, strips out comments and whitespace, and validates code.

A little more work up front saves much grief down the line.

A few interesting tidbits showed up in the Q&A session at the end. Zakas said he didn't like most IDEs, but was "very impressed by the new Java support in new versions of Visual Studio, the free version called Visual Web Developers Express." It has a lot of hooks so you can put in JS comments it can pick up. It's in beta.

Someone asked him if there were exceptions to his rules. He said sure. For example, "there definitely will be cases where you need to generate some markup in HTML -- but always question it."

About the Author

Lee Thé's first computer was a state-of-the-art unit with 48K RAM and a 1MHz processor. He has been writing and editing computer magazine articles since then, in between scuba diving trips. He's based in the San Francisco Bay Area.