You don’t need to know the arcane inner workings of JavaScript in order to do lots of useful things with it. However, if you plan to write an application with tens of thousands of lines of code, you’re going to need to explore a little deeper. Here you’ll explore some fundamentals that will help you build large applications that are still maintainable. For more on these subjects, I strongly recommend JavaScript: The Good Parts by Douglas Crockford.
In the real world, applications are fragile. When you change them, things often break, and you need an easy way to detect those broken things so you can fix them before your users are impacted.
Manual quality assurance is an expensive, time-consuming, and tedious process, and the larger your code base grows, the bigger the job gets. One commonly accepted solution to this problem is to write tests that assure your code does what you expect.
Tests can aid in debugging as well. Frequently when a large number of components interact (often indirectly), it’s difficult to tell which component a bug is hiding in. If you have a test suite, and one component breaks, you’ll be alerted immediately to which component isn’t behaving the way it was designed to behave.
Throughout this book you’ll see QUnit tests in examples to alert you to expected and actual behaviors. QUnit is a JavaScript unit test framework that generates clear, readable test output on whatever page you include it in. It’s the test suite used by jQuery.
It is safe to assume that all tests pass, unless you see a comment that says otherwise. For example:
var
highPass
=
function
highPass
(
number
,
cutoff
)
{
if
(
number
>=
cutoff
)
{
return
true
;
}
else
{
return
false
;
}
},
lowPass
=
function
lowPass
(
number
,
cutoff
)
{
if
(
number
>=
cutoff
)
{
return
true
;
}
else
{
return
false
;
}
};
module
(
'Filter Examples'
);
test
(
'highPass'
,
function
()
{
ok
(
!
highPass
(
2
,
5
),
'Lower values should not pass.'
);
ok
(
highPass
(
8
,
5
),
'Higher values should pass.'
);
});
test
(
'lowPass'
,
function
()
{
ok
(
lowPass
(
2
,
5
),
'Lower values should pass.'
);
// Fails
ok
(
!
lowPass
(
8
,
5
),
'Higher values should not pass.'
);
// Fails
});
The first test passes the tests as written. The second test set
fails because the logic needs to be inverted in
lowPass()
:
if
(
number
>=
cutoff
)
{
becomes:
if
(
number
<=
cutoff
)
{
This book demonstrates several QUnit functions:
module()
test()
ok()
equal()
The module()
function allows you to group related functionality in your test
output. The name
parameter can be any string:
module(
'Name'
);
The test()
function lets you name and define individual module tests. All assertions
go inside tests:
test('Name'
, function () {// Your assertions here...
});
You can test the truthiness of an expression with ok():
ok(true
,'Description'
);
The first parameter can be any expression you wish to evaluate. If
the expression is truthy, the assertion will pass. You can do anything you
want with the description, but I like to write them like:
Subject
should verb
.
For example, “User should exist,” “Valid input should pass,” etc. If you
find yourself writing “Subject
should
be...” consider using equal()
instead.
The equal()
function allows you to compare the equality of two values. For
example:
var
a
=
true
;
equal
(
a
,
true
,
'a should be true.'
);
When you approach JavaScript for application development, style becomes more important than it is in the context of one-off scripts. Code is more readable if code style and conventions are used consistently. That’s not to say that your style shouldn’t evolve as you hone your skills: rather, it should converge on a set of standards that other developers can learn and use.
Some style choices are arbitrary, but many best practices have emerged in the JavaScript community. In this section, we’ll cover a few of the more important style considerations you should be aware of.
Perhaps the easiest way to start converging on a code quality standard is to begin linting your code with a tool like JSLint. Linters scan your source code for syntax errors (and in the case of JSLint, some stylistic errors, as well).
The most obvious reason to use a lint tool is that it can be an extremely valuable debugging asset. Most bugs are the result of syntax errors. If a tool can help you eliminate all syntax-related bugs from your code quickly, you’ll see huge gains in productivity.
A few of the style choices enforced in JSLint have raised controversy in the JavaScript community, but even if you don’t agree with every point that the author, Douglas Crockford, makes about JavaScript style, it’s important to have a thorough understanding of why those choices were made.
More importantly, using an automated tool like JSLint is the best way to enforce a minimum level of code quality and style throughout the code base. In my experience, getting all the developers on the same page is worth a little sacrifice in style.
If you can make the trade-offs, what you gain in increased productivity is extraordinary. However, if you can’t make one or two of the trade-offs prescribed by JSLint, there is a tool for you, as well: JSHint. In JSHint, the rules are more flexible.
Caution
Think carefully before choosing to ignore the recommendations of JSLint. While some of them may seem arbitrary at first, they’re all there for a reason.
Any time somebody starts dictating style, there is an inevitable backlash. Douglas Crockford is famously unapologetic about the style dictation enforced by JSLint and firmly believes that everybody should be using it as he prefers to have it configured. I have a lot of respect for the work that he put into it, but my view on style is a bit more flexible.
Some of the recommendations here are more important than others, but all of them are about improving the quality and consistency of your code. As you learn new things over the years, you should learn better ways to accomplish those goals. This section is not meant to be taken as gospel. Instead, it’s meant to get you thinking and help you understand ways that you can improve the overall quality of the code you write. If your style never deviates from the style I recommend in this book, I believe that would be unfortunate. I don’t want to see JavaScript style stagnate; I want to see it improve over time as we learn new things as a community.
Tabs or spaces? It’s common for people to have very strong feelings, but both have their merits. Tab users say it saves them keystrokes, there are never any hidden whitespace characters, and tabs allow users to set their own indentation display preferences. Space users say that most editors will automatically substitute spaces for tabs and support “soft tabs” that have similar display characteristics to tabs. And you can line up your code better with more fine-grained control of your whitespace. All good points. Too close to call a clear winner.
What’s important is that you pick one and stick with it. If you’re working as a team, all members of the team should use the same standard.
There is a recent and growing movement in JavaScript circles to use a style that leaves off optional semicolons. When the JavaScript runtime encounters a condition where there should be a semicolon to make sense of the code but can’t find one, it inserts a semicolon automatically. This behavior is called automatic semicolon insertion (ASI).
The new style depends on ASI to produce valid JavaScript code for the engine to execute. In my opinion, this style should be avoided in the short term but maybe embraced in the long term.
The problem with the new style is not in the style itself. Provided you follow a few simple rules, it is a readable and viable. Less syntax is sometimes more.
The problem is that there are currently no automated lint tools that I am aware of which can ignore your missing semicolons when they’re not required but highlight the error cases where leaving out the semicolon will cause ambiguities that ASI can’t resolve. Lint tools are highly valuable, and missing semicolons are among the most common sources of bugs. I can’t recommend a style that would require you to abandon useful lint rules.
Whatever you do, don’t just leave all semicolons out. Sometimes, semicolons are required. The following tests all fail due to missing semicolons:
test
(
'Missing semicolon before +'
,
function
()
{
var
a
=
1
+
1
+
'3'
;
equal
(
a
,
2
,
'a should be 2.'
);
// Fails. a = 23
});
test
(
'Missing semicolon before ['
,
function
()
{
var
b
=
1
+
1
[
1
,
1
].
forEach
(
function
(
num
)
{
b
+=
num
;
});
// Error: Cannot call method forEach of undefined.
equal
(
b
,
4
,
'b should be 4.'
);
});
test
(
'Missing semicolon before ('
,
function
()
{
var
x
=
1
,
f
=
function
f
()
{
ok
(
false
,
'This test should not run.'
);
// Fails.
}
(
function
()
{
/* Do something interesting. */
}());
});
Due to automatic semicolon insertion, brackets should always be placed to
the right, on the line that begins the statement in the first
getLocation()
.
Wrong:
var
getLocation
=
function
getLocation
()
{
return
{
x
:
20
,
y
:
20
};
};
Right:
var
getLocation
=
function
getLocation
()
{
return
{
x
:
20
,
y
:
20
};
};
The default scope for variables in JavaScript is global. Every other script on the page has to share that namespace. In order to prevent name collisions, your application should use as few global names as possible. Typically, that would be a single name for the application and possibly a handful of others for supporting libraries, such as jQuery and Underscore.
Wrong: x
is global:
var
x
;
// Do stuff with x...
Right: x
is local to the wrapping
function:
(
function
myScript
()
{
var
x
;
// Do stuff with x...
}());
This type of function wrapper is called an Immediately Invoked Function Expression (IIFE, pronounced “iffy”). For details, see Immediately Invoked Function Expressions.
Modules are the best way to avoid name collisions in JavaScript. See Chapter 4. The preceding code is an example of the module pattern described in the modules chapter, but there are several other types of modules, including AMD and Node-style modules.
If you forget to use the var
keyword, your variable assignments
will pollute the global scope.
Wrong: x
is global:
var
add
=
function
add
(
number
)
{
x
=
2
;
return
number
+
x
;
};
test
(
'Without var.'
,
function
()
{
equal
(
add
(
2
),
4
,
'add() should add 2 to whatever you pass in.'
);
// Fails
ok
(
!
x
,
'x should not pollute the global scope.'
);
});
Right: function scope:
var
add
=
function
add
(
number
)
{
var
x
=
2
;
return
number
+
x
;
};
test
(
'With var.'
,
function
()
{
equal
(
add
(
2
),
4
,
'add() should add 2 to whatever you pass in.'
);
ok
(
!
x
,
'x should not pollute the global scope.'
);
});
Because of the peculiarities with hoisting (see Hoisting), you
should only use one var
statement per function, at the top
of your function. Some style guides disagree with me on this, but in
practice, every time I see multiple var
statements
used as a matter of style on a team with several developers, variable
declarations tend to slip down away from the top of the function, which
opens the door to hoisting bugs.
Wrong: multiple var
statements:
(
function
myScript
()
{
var
x
=
true
;
var
y
=
true
;
/* do some stuff with x and y */
var
z
=
true
;
/* do some stuff with z */
}());
Right: one var
per function:
(
function
myScript
()
{
var
x
=
true
,
y
=
true
,
z
=
true
;
/* do some stuff with x, y, and z */
}());
Constants are a class of types that allow you to specify names for values that never change. They’re used to express things like mathematical constants (like pi), fixed ratios (the number of seconds in a minute), etc. If a constant is able to change, it will likely break the code that relies on it. It is for this reason that in most languages, constants are immutable.
That is not the case in JavaScript. There is a const
keyword coming to JavaScript, but it will likely be a few years before
you can safely use it for cross-browser code. This hasn’t stopped people
from faking constants in JavaScript, however.
The general convention is to use all caps to name constants. This convention is much older than JavaScript, and the well-understood meaning is, “You can count on this value to never change, and to never need changing.” Because you can’t count on fake constants to be constant, it’s probably better not to fake it at all. If you really need the value to be safe, store it in a closure and use a getter function to access it. See Closures.
More often than not in practice, I’ve seen fake constants abused for things that never should have been considered constant in the first place. It’s very common to confuse constants and configuration.
Code that employs constants instead of configuration tends to get more rigid and brittle as it grows. If you can’t configure a module, the same code tends to get rewritten with few differences, except for variable names and a few differences in constant declarations.
Employing configuration instead of constants allows you to reuse the code, instead of writing new code.
Wrong: abusing constants:
var
createUserBox
=
function
createUserBox
()
{
var
WIDTH
=
'80px'
,
HEIGHT
=
'80px'
;
return
{
width
:
WIDTH
,
height
:
HEIGHT
};
},
createMenuBox
=
function
createMenuBox
()
{
var
WIDTH
=
'200px'
,
HEIGHT
=
'400px'
;
return
{
width
:
WIDTH
,
height
:
HEIGHT
};
};
test
(
'Avoid Constants'
,
function
()
{
var
userBox
=
createUserBox
(),
menuBox
=
createMenuBox
();
equal
(
userBox
.
width
,
'80px'
,
'userBox should be 80px wide.'
);
equal
(
menuBox
.
width
,
'200px'
,
'menuBox should be 200px wide.'
);
});
Right: favor configuration:
var
createBox
=
function
createBox
(
options
)
{
var
defaults
=
{
width
:
'80px'
,
height
:
'80px'
};
return
$
.
extend
({},
defaults
,
options
);
};
test
(
'Avoid Constants'
,
function
()
{
var
userBox
=
createBox
({
width
:
'80px'
,
height
:
'80px'
}),
menuBox
=
createBox
({
width
:
'200px'
,
height
:
'400px'
});
equal
(
userBox
.
width
,
'80px'
,
'userBox should be 80px wide.'
);
equal
(
menuBox
.
width
,
'200px'
,
'menuBox should be 200px wide.'
);
});
JavaScript offers the capability to program in a functional style, meaning that you can pass functions as arguments to other functions and apply the passed in function to each element in a data resource (array or object). The technique is handy because it can make your code more readable, abstract away the looping mechanics, and keep the loop control variables isolated from your main function. That said, loops can give you finer-grained control, and there can be performance considerations when you’re looping over very large datasets.
Good:
var
getCount
=
function
getCount
()
{
var
i
,
count
=
[
1
,
2
,
3
],
length
=
count
.
length
,
text
=
''
;
for
(
i
=
0
;
i
<
length
;
i
+=
1
)
{
text
+=
count
[
i
]
+
' '
;
}
return
text
;
};
test
(
'Looping over an array:'
,
function
()
{
var
text
=
getCount
();
equal
(
text
,
'1 2 3 '
,
'getCount() should count to three.'
);
});
Better for most situations:
var
getCount
=
function
getCount
()
{
var
count
=
[
1
,
2
,
3
],
text
=
''
;
count
.
forEach
(
function
(
number
)
{
text
+=
number
+
' '
;
});
return
text
;
};
test
(
'Functional iterator:'
,
function
()
{
var
text
=
getCount
();
equal
(
text
,
'1 2 3 '
,
'getCount() should count to three.'
);
});
Note that you don’t need to worry about the i
or
length
variables in this version.
Even better: use the right utility for the job, and you can eliminate even more variables and avoid mutating variables outside the lambda:
var getCount = function getCount() { var count = [1, 2, 3]; return count.reduce(function (previous, number) { return previous + number + ' '; }, ''); };
Functional iterators were added in ECMAScript 5, meaning that
you’ll need to shim them to prevent throwing errors in older browsers.
Here’s the MDN .forEach()
shim:
// Shim .forEach()
if
(
!
Array
.
prototype
.
forEach
)
{
Array
.
prototype
.
forEach
=
function
(
fn
,
scope
)
{
for
(
var
i
=
0
,
len
=
this
.
length
;
i
<
len
;
++
i
)
{
fn
.
call
(
scope
||
this
,
this
[
i
],
i
,
this
);
}
};
}
Tip
A shim is any code that wraps a standard API call in order to abstract it. Shims can be used to normalize cross-browser behavior. A polyfill is a shim that implements new standard features from JavaScript, HTML5, and related technologies using existing capabilities in order to deliver that new functionality to older browsers. To learn more about using JavaScript polyfills, search for any new feature in the JavaScript method descriptions on the Mozilla Developer Network (MDN). Most of the new features include a complete or partial polyfill in the description. If you want to make it really easy, try the ES5-shim project.
The MDN documentation is perhaps the most thorough and authoritative online JavaScript reference. If you add the keyword “MDN” when you search Google for JavaScript features, it should pull up the MDN documentation instead of other much less reliable and much less complete references online.
The results of type coercion in JavaScript can be difficult to predict, even when you know the rules. Here you’ll see how subtle differences in comparisons can lead to significant changes in the results.
Be careful when relying on truthiness for logic. Whenever possible, use comparison operators to clarify your meaning. Falsy values include:
0
undefined
null
empty strings
false
Caution
Boolean objects instantiated with new
Boolean(false)
are truthy because they’re
objects, and objects are truthy. In order to test against them you
need to use the .valueOf()
method. It’s better to use
true
and false
instead.
Empty objects and empty arrays are truthy, including objects
created with new Boolean(false)
.
Wrong: don’t use new Boolean()
:
var
myBool
=
new
Boolean
(
false
);
test
(
'Boolean object'
,
function
()
{
ok
(
!
myBool
,
'Should be falsy'
);
// Fails
ok
(
!
myBool
.
valueOf
(),
'Should be falsy.'
);
// Passes
});
Right: use true
or false
in your boolean
declarations:
var
myBool
=
false
;
test
(
'Boolean object'
,
function
()
{
ok
(
!
myBool
,
'!myBool should be false.'
);
});
The following series of tests are meant to demonstrate how different types of comparisons in JavaScript that look similar can deliver very different results. Always be careful that you’re using the correct test for your situation:
function
truthy
(
x
)
{
if
(
x
)
{
return
true
;
}
else
{
return
false
;
}
}
test
(
'Truthy'
,
function
()
{
// Falsy
equal
(
truthy
(
0
),
true
,
'truthy(0)'
);
// Fail
equal
(
truthy
(
''
),
true
,
"truthy('')"
);
// Fail
equal
(
truthy
(
null
),
true
,
'truthy(null)'
);
// Fail
equal
(
truthy
(
undefined
),
true
,
'truthy(undefined)'
);
// Fail
equal
(
truthy
(
false
),
true
,
'truthy(false)'
);
// Fail
// Truthy
equal
(
truthy
(
'0'
),
true
,
"truthy('0')"
);
// Pass
equal
(
truthy
(
new
Boolean
(
false
)),
true
,
'truthy(new Boolean(false))'
);
// Pass
equal
(
truthy
({}),
true
,
'truthy({})'
);
// Pass
equal
(
truthy
([]),
true
,
'truthy([])'
);
// Pass
equal
(
truthy
([
0
]),
true
,
'truthy([0])'
);
// Pass
equal
(
truthy
([
1
]),
true
,
'truthy([1])'
);
// Pass
equal
(
truthy
([
'0'
]),
true
,
"truthy(['0'])"
);
// Pass
equal
(
truthy
([
'1'
]),
true
,
"truthy(['1'])"
);
// Pass
});
These are the falsy and truthy values we’ll use as a baseline for a series of other comparisons.
Caution
Often, developers will use if (x)
when they really
want to see if the value has been set at all. This is especially
problematic when 0
, empty strings, or false
are valid values. In that case, you want the test to pass as long as
the expression evaluates to anything other than null
or
undefined
. A good rule of thumb is to only use if
(x)
to evaluate booleans or the existence of objects or
arrays.
function
exists
(
x
)
{
if
(
x
!==
undefined
&&
x
!==
null
)
{
return
true
;
}
else
{
return
false
;
}
}
test
(
'exists'
,
function
()
{
// Falsy
equal
(
exists
(
0
),
true
,
'exists(0)'
);
// Pass
equal
(
exists
(
''
),
true
,
"exists('')"
);
// Pass
equal
(
exists
(
null
),
true
,
'exists(null)'
);
equal
(
exists
(
undefined
),
true
,
'exists(undefined)'
);
equal
(
exists
(
false
),
true
,
'exists(false)'
);
// Pass
// Truthy
equal
(
exists
(
'0'
),
true
,
"exists('0')"
);
// Pass
equal
(
exists
(
new
Boolean
(
false
)),
true
,
'exists(new Boolean(false))'
);
// Pass
equal
(
exists
({}),
true
,
'exists({})'
);
// Pass
equal
(
exists
([]),
true
,
'exists([])'
);
// Pass
equal
(
exists
([
0
]),
true
,
'exists([0])'
);
// Pass
equal
(
exists
([
1
]),
true
,
'exists([1])'
);
// Pass
equal
(
exists
([
'0'
]),
true
,
"exists(['0'])"
);
// Pass
equal
(
exists
([
'1'
]),
true
,
"exists(['1'])"
);
// Pass
});
Of course, a shorter version of that function will return the same results:
function
exists
(
x
)
{
return
(
x
!==
undefined
&&
x
!==
null
);
}
Caution
The ==
operator can return some problematic results
due to type coercion. For example, say you’re checking an array to be
sure it contains a valid price, and some items cost $0.00. Your code
could fail because ['0.00'] == false
. Use
===
instead.
var
isFalse
=
function
isFalse
(
x
)
{
return
(
x
==
false
);
};
test
(
'isFalse using =='
,
function
()
{
// Falsy
equal
(
isFalse
(
0
),
true
,
'isFalse(0)'
);
// Pass
equal
(
isFalse
(
''
),
true
,
"isFalse('')"
);
// Pass
equal
(
isFalse
(
null
),
true
,
'isFalse(null)'
);
// Fail
equal
(
isFalse
(
undefined
),
true
,
'isFalse(undefined)'
);
// Fail
equal
(
isFalse
(
false
),
true
,
'isFalse(false)'
);
// Pass
// Truthy
equal
(
isFalse
(
'0'
),
true
,
"isFalse('0')"
);
// Pass
equal
(
isFalse
(
new
Boolean
(
false
)),
true
,
'isFalse(new Boolean(false))'
);
// Pass
equal
(
isFalse
({}),
true
,
'isFalse({})'
);
// Fail
equal
(
isFalse
([]),
true
,
'isFalse([])'
);
// Pass
equal
(
isFalse
([
0
]),
true
,
'isFalse([0])'
);
// Pass
equal
(
isFalse
([
1
]),
true
,
'isFalse([1])'
);
// Fail
equal
(
isFalse
([
'0'
]),
true
,
"isFalse(['0'])"
);
// Pass
equal
(
isFalse
([
'1'
]),
true
,
"isFalse(['1'])"
);
// Fail
});
The following code will only return true if x
is
actually set to false
:
var
isFalse
=
function
isFalse
(
x
)
{
return
(
x
===
false
);
};
The following function is dangerous, because it will return
true
for 1
, [1]
, and
['1']
:
var
isTrue
=
function
isTrue
(
x
)
{
return
(
x
==
true
);
};
Use ===
instead to eliminate the possibility of false
positives.
Remember that if (x)
will always return true when
x
is an object—even if the object is empty. It’s common to
use ducktyping when you examine objects. (If it walks like a duck and talks
like a duck, treat it like a duck.) The idea is similar to using feature
detection in browsers instead of running checks against the browser
string. Ducktyping is feature detection for objects:
if
(
typeof
foo
.
bar
===
'function'
)
{
foo
.
bar
();
}
In order to avoid unintended side effects, only assign to variables declared inside your function, or declared as formal parameters. See Minimize Side Effects.
Wrong: has side effects:
var
x
=
0
;
function
increment
()
{
x
+=
1
;
return
x
;
}
test
(
'increment() with side effects.'
,
function
()
{
var
val
=
increment
();
equal
(
val
,
1
,
'increment() should add one.'
);
equal
(
x
,
0
,
'x should be unchanged outside the function.'
);
// Fails
});
Right: no side effects:
var
x
=
0
;
function
increment
(
x
)
{
x
+=
1
;
return
x
;
}
test
(
'increment() without side effects.'
,
function
()
{
var
val
=
increment
(
x
);
equal
(
val
,
1
,
'increment() adds one.'
);
equal
(
x
,
0
,
'x is unchanged.'
);
});
Wherever possible, you should avoid mutating objects declared outside your function. This isn’t always practical, but where it is, your code will be more reusable and less brittle (likely to break when unrelated code changes).
Use with caution: object-mutation side effects:
var
obj
=
{
value
:
2
};
function
setValue
(
obj
,
value
)
{
obj
.
value
=
value
;
return
obj
;
}
test
(
'setValue() with side effects'
,
function
()
{
var
myObj
=
setValue
(
obj
,
3
);
equal
(
myObj
.
value
,
3
,
'setValue() returns new value.'
);
equal
(
obj
.
value
,
2
,
'The original should be unchanged.'
);
// Fails
});
Better: return a new object:
var
obj
=
{
value
:
2
};
function
setValue
(
obj
,
value
)
{
// Note: extend() is jQuery.extend(),
// Underscore .extend(), or similar...
var
instance
=
extend
({},
obj
);
return
instance
;
}
test
(
'setValue() without side effects'
,
function
()
{
var
myObj
=
setValue
(
obj
,
3
);
equal
(
myObj
.
value
,
3
,
'setValue() should return new value.'
);
equal
(
obj
.
value
,
2
,
'The original is should be unchanged!'
);
// Passes
});
JavaScript has pretty normal control-flow statements that use blocks
delineated by curly braces. There is an exception to this: the
switch ... case
statement. The strange thing about
switch ... case
is that you must include the keyword
break
at the end of each case
to prevent
control from falling through to the next case
.
Fall through is a trick that allows you to let
more than one case be executed. Control will fall through automatically
to the next case unless you explicitly tell it not to with
break
. However, like the optional semicolons and curly
braces, it’s possible to forget break
when you really
should have used it. When that happens, the bug is difficult to find
because the code looks correct. For that reason,
the break
statement should never be left off of a
case
, even by design.
With that said, JavaScript has an elegant object-literal syntax and first-class functions, which makes it simple to create a keyed method lookup. The object you create for your method lookup is called an action object or command object and is used in many software design patterns.
Say you’re creating a game where the nonplayer fight actions are
selected based on an algorithm defined elsewhere and passed in to
doAction
as a string. The switch ...
case
form looks like this:
function
doAction
(
action
)
{
switch
(
action
)
{
case
'hack'
:
return
'hack'
;
break
;
case
'slash'
:
return
'slash'
;
break
;
case
'run'
:
return
'run'
;
break
;
default
:
throw
new
Error
(
'Invalid action.'
);
break
;
}
}
The method lookup version looks like this:
function
doAction
(
action
)
{
var
actions
=
{
'hack'
:
function
()
{
return
'hack'
;
},
'slash'
:
function
()
{
return
'slash'
;
},
'run'
:
function
()
{
return
'run'
;
}
};
if
(
typeof
actions
[
action
]
!==
'function'
)
{
throw
new
Error
(
'Invalid action.'
);
}
return
actions
[
action
]();
}
Or, for input grouping (a frequent use case for the fall-through feature): say you’re writing a programming language parser, and you want to perform one action whenever you encounter a token that opens an object or array, and another whenever you encounter a token that closes them. Assume the following functions exist:
function
handleOpen
(
token
)
{
return
'Open object / array.'
;
}
function
handleClose
(
token
)
{
return
'Close object / array'
;
}
The switch ... case
form is:
function
processToken
(
token
)
{
switch
(
token
)
{
case
'{'
:
case
'['
:
handleOpen
(
token
);
break
;
case
']'
:
case
'}'
:
handleClose
(
token
);
break
;
default
:
throw
new
Error
(
'Invalid token.'
);
break
;
}
}
The method lookup version looks like this:
var
tokenActions
=
{
'{'
:
handleOpen
,
'['
:
handleOpen
,
']'
:
handleClose
,
'}'
:
handleClose
};
function
processToken
(
token
)
{
if
(
typeof
tokenActions
[
token
]
!==
'function'
)
{
throw
new
Error
(
'Invalid token.'
);
}
return
tokenActions
[
token
](
token
);
}
At first glance, it might seem like this is more complicated syntax, but it has a few advantages:
It uses the standard curly-bracket blocks used everywhere else in JavaScript.
You never have to worry about remembering the
break
.Method lookup is much more flexible. Using an action object allows you to alter the cases dynamically at runtime, for example, to allow dynamically loaded modules to extend cases, or even swap out some or all of the cases for modal context switching.
Method lookup is object oriented by definition. With
switch ... case
, your code is more procedural.
The last point is perhaps the most important. The
switch
statement is a close relative of the
goto
statement, which computer scientists argued for 20
years to eradicate from modern programming languages. It has the same
serious drawback: almost everywhere I’ve seen switch ...
case
used, I’ve seen it abused. Developers group unrelated
functionality into overly-clever branching logic. In other words,
switch ... case
tends to encourage spaghetti
code, while method lookup tends to encourage well-organized,
object-oriented code. It’s far too common to find implementations of
switch ... case
, which violate the principles of high
cohesion and separation of
concerns.
I was once a fan of switch ... case
as a better
alternative to if ... else
, but after becoming more
familiar with JavaScript, I naturally fell into using method lookup
instead. I haven’t used switch ... case
in my code for
several years. I don’t miss it at all.
If you ever find yourself writing a switch
statement,
stop and ask yourself the following questions:
Will you ever need to add more cases? (queue, stack, plug-in architecture)
Would it be useful to modify the list of cases at runtime, for example, to change the list of enabled options based on context? (mode switching)
Would it be useful to log the cases that get executed, for example, to create an undo/redo stack, or log user actions to your servers for analysis? (command manager)
Are you referencing your cases by incrementing numbers, for example,
case 1:
,case: 2
, etc.? (iterator target)Are you trying to group related inputs together with the fall through feature so that they can share code?
If you answered yes to any of these questions, there is almost
certainly a better implementation that doesn’t utilize
switch
or its slippery fall-through feature.
The desire to use eval()
should be considered a
code smell (as in, “something smells fishy”).
It’s a good indication that there is probably a better way to accomplish
what you’re after.
Tip
You’ll often hear programmers say things like, “this smells,” or “something smells funny here.” They’re not talking about the garlic burger you had for lunch; they’re expressing that something doesn’t feel right about the code. Maybe a function is too long, or it’s doing too much. Maybe there’s a better, smarter, faster, or easier way to accomplish the same goal. Things like large inheritance trees and deeply nested logic send up red flags for experienced programmers. Those are code smells.
JavaScript is a very flexible, expressive, dynamic language with a
lot less need for
than most other
languages.eval()
Most of what eval()
can do for you can be done with
code that is faster and easier to read. Using eval()
forces
the JavaScript engine to delay compilation of the eval()
code, bypassing the usual code compilation flow and hindering compiler
optimizations.
eval()
can also poke holes in your security and
allow cross-site scripting (XSS) attacks.
Some good alternatives to eval()
and its
dopplegangers include square bracket notation and runtime object
mutation. The eval()
function should only be used with
great caution, and only if it is absolutely the only way to accomplish
your goal.
eval()
is also prohibited in many places where
application security is a concern. For instance, eval()
is
not allowed in Chrome
extensions unless it’s sandboxed.
Caution
The eval()
function has dopplegangers you should
also avoid:
Passing a string to the
Function()
constructorPassing a string value to
setTimeout()
orsetInterval()
(as opposed to a function reference or lambda expression)
Get Programming JavaScript Applications now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.