1. Writing Maintainable Code
Software bugs are costly to fix. And their cost increases over
time, especially if the bugs creep into the publicly released product.
It’s best if you can fix a bug right away, as soon you find it; this is
when the problem your code solves is still fresh in your head. Otherwise
you move on to other tasks and forget all about that particular code.
Revisiting the code after some time has passed requires:
Another problem, specific to bigger projects or companies, is that
the person who eventually fixes the bug is not the same person who
created the bug (and also not the same person who found the bug). It’s
therefore critical to reduce the time it takes to understand code,
either written by yourself some time ago or written by another developer
in the team. It’s critical to both the bottom line (business revenue)
and the developer’s happiness, because we would all rather develop
something new and exciting instead of spending hours and days
maintaining old legacy code.
Another fact of life related to software development in general is
that usually more time is spent reading code than
writing it. In times when you’re focused and deep
into a problem, you can sit down and in one afternoon create a
considerable amount of code. The code will probably work then and there,
but as the application matures, many other things happen that require
your code to be reviewed, revised, and tweaked. For example:
Bugs are uncovered.
New features are added to the application.
The application needs to work in new environments (for
example, new browsers appear on the market).
The code gets repurposed.
The code gets completely rewritten from scratch or ported to
another architecture or even another language.
As a result of the changes, the few man-hours spent writing the
code initially end up in man-weeks spent reading it. That’s why creating
maintainable code is critical to the success of an application.
Maintainable code means code that:
2. Minimizing Globals
JavaScript uses functions to manage scope. A variable declared inside of a function is
local to that function and not available outside the function. On the
other hand, global variables are those declared
outside of any function or simply used without being declared.
Every JavaScript environment has a global object accessible when you
use this outside of any function.
Every global variable you create becomes a property of the global
object. In browsers, for convenience, there is an additional property of
the global object called window that (usually)
points to the global object itself. The following code snippet shows how
to create and access a global variable in a browser environment:
myglobal = "hello"; // antipattern
console.log(myglobal); // "hello"
console.log(window.myglobal); // "hello"
console.log(window["myglobal"]); // "hello"
console.log(this.myglobal); // "hello"
2.1. The Problem with Globals
The problem with global variables is that they are shared among all the
code in your JavaScript application or web page. They live in the same
global namespace and there is always a
chance of naming collisions—when two separate parts of an application
define global variables with the same name but with different
purposes.
It’s also common for web pages to include code not written by
the developers of the page, for example:
A third-party JavaScript library
Scripts from an advertising partner
Code from a third-party user tracking and analytics
script
Different kinds of widgets, badges, and buttons
Let’s say that one of the third-party scripts defines a global
variable, called, for example, result. Then later in one of your functions
you define another global variable called result. The outcome of that is the last
result variable overwrites the
previous ones, and the third-party script may just stop
working.
Therefore it’s important to be a good neighbor to the other
scripts that may be in the same page and use as few global variables
as possible.
It is surprisingly easy to create globals involuntarily because
of two JavaScript features. First, you can use variables without even
declaring them. And second, JavaScript has the notion of implied globals, meaning that any
variable you don’t declare becomes a property of the global object
(and is accessible just like a properly declared global variable).
Consider the following example:
function sum(x, y) {
// antipattern: implied global
result = x + y;
return result;
}
In this code, result is used
without being declared. The code works fine, but after calling the
function you end up with one more variable result in the global namespace that can be a
source of problems.
The rule of thumb is to always declare variables with var,
as demonstrated in the improved version of the sum() function:
function sum(x, y) {
var result = x + y;
return result;
}
Another antipattern that creates implied globals is to chain assignments as part of a
var declaration. In the following
snippet, a is local but b becomes global, which is probably not what
you meant to do:
// antipattern, do not use
function foo() {
var a = b = 0;
// ...
}
If you’re wondering why that happens, it’s because of the
right-to-left evaluation. First, the expression b = 0 is evaluated and in this case b is not declared. The return value of this
expression is 0, and it’s assigned to the new local variable declared
with var a. In other words, it’s as
if you’ve typed:
var a = (b = 0);
If you’ve already declared the variables, chaining assignments is fine and
doesn’t create unexpected globals. Example:
function foo() {
var a, b;
// ...
a = b = 0; // both local
}
Note:
Yet another reason to avoid globals is portability. If you
want your code to run in different environments (hosts), it’s
dangerous to use globals because you can accidentally overwrite a
host object that doesn’t exist in your original environment (so you
thought the name was safe to use) but which does in some of the
others.
2.2. Side Effects When Forgetting var
There’s one slight difference between implied globals and explicitly
defined ones—the difference is in the ability to undefine these
variables using the delete
operator:
This shows that implied globals are technically not real
variables, but they are properties of the global object. Properties
can be deleted with the delete operator
whereas variables cannot:
// define three globals
var global_var = 1;
global_novar = 2; // antipattern
(function () {
global_fromfunc = 3; // antipattern
}());
// attempt to delete
delete global_var; // false
delete global_novar; // true
delete global_fromfunc; // true
// test the deletion
typeof global_var; // "number"
typeof global_novar; // "undefined"
typeof global_fromfunc; // "undefined"
In ES5 strict mode, assignments to undeclared variables (such as
the two antipatterns in the preceding snippet) will throw an
error.
2.3. Access to the Global Object
In the browsers, the global object is accessible from any part of the code
via the window property (unless you’ve done
something special and unexpected such as declaring a local variable
named window). But in other
environments this convenience property may be called something else
(or even not available to the programmer). If you need to access the
global object without hard-coding the identifier window, you can do the following from any
level of nested function scope:
var global = (function () {
return this;
}());
This way you can always get the global object, because inside
functions that were invoked as functions (that is, not as constructors
with new) this should always point to the global object. This is
actually no longer the case in ECMAScript 5 in strict mode, so you
have to adopt a different pattern when your code is in strict mode.
For example, if you’re developing a library, you can wrap your library
code in an immediate function and then from the global scope, pass a
reference to this as a parameter to
your immediate function.
2.4. Single var Pattern
Using a single var statement at the top of your functions is a useful
pattern to adopt. It has the following benefits:
Provides a single place to look for all the local
variables needed by the function
Prevents logical errors when a variable is used before
it’s defined (see Section 2.2.5)
Helps you remember to declare variables and therefore
minimize globals
Is less code (to type and to transfer over the
wire)
The single var pattern looks like this:
function func() {
var a = 1,
b = 2,
sum = a + b,
myobject = {},
i,
j;
// function body...
}
You use one var statement and
declare multiple variables delimited by commas. It’s a good practice
to also initialize the variable with an initial
value at the time you declare it. This can prevent logical errors (all
uninitialized and declared variables are initialized with the value
undefined) and also improve the
code readability. When you look at the code later, you can get an idea
about the intended use of a variable based on its initial value—for
example, was it supposed to be an object or an integer?
You can also do some actual work at the time of the declaration,
like the case with sum = a + b in
the preceding code. Another example is when working with DOM (Document
Object Model) references. You can assign DOM references to local
variables together with the single declaration, as the following code
demonstrates:
function updateElement() {
var el = document.getElementById("result"),
style = el.style;
// do something with el and style...
}
2.5. Hoisting: A Problem with Scattered vars
JavaScript enables you to have multiple var statements anywhere in a function, and
they all act as if the variables were declared at the top of the
function. This behavior is known as hoisting. This can lead to
logical errors when you use a variable and then you declare it further
in the function. For JavaScript, as long as a variable is in the same
scope (same function), it’s considered declared, even when it’s used
before the var declaration. Take a
look at this example:
// antipattern
myname = "global"; // global variable
function func() {
alert(myname); // "undefined"
var myname = "local";
alert(myname); // "local"
}
func();
In this example, you might expect that the first alert() will prompt “global” and the second
will prompt “local.” It’s a reasonable expectation because, at the
time of the first alert, myname was
not declared and therefore the function should probably “see” the
global myname. But that’s not how
it works. The first alert will say “undefined” because myname is considered declared as a local
variable to the function. (Although the declaration comes after.) All
the variable declarations get hoisted to the top of the function.
Therefore to avoid this type of confusion, it’s best to declare
upfront all variables you intend to use.
The preceding code snippet will behave as if it were implemented
like so:
myname = "global"; // global variable
function func() {
var myname; // same as -> var myname = undefined;
alert(myname); // "undefined"
myname = "local";
alert(myname); // "local"
}
func();
Note:
For completeness, let’s mention that actually at the
implementation level things are a little more complex. There are two
stages of the code handling, where variables, function declarations,
and formal parameters are created at the first stage, which is the
stage of parsing and entering the context. In the second stage, the
stage of runtime code execution, function expressions and
unqualified identifiers (undeclared variables) are created. But for
practical purposes, we can adopt the concept of
hoisting, which is actually not defined by
ECMAScript standard but is commonly used to describe the
behavior.