Chapter 4. Variables, Functions, and Flow Control
Introduction
This chapter covers a miscellany of core JavaScript topics. A couple of these recipes (or your own variations on them) may be part of your daily menu. If you don’t use these constructions frequently, let this chapter serve to refresh your memory, and give you models to get you back on track when you need them.
Even simple subjects, such as JavaScript variables and functions,
have numerous nuances that are easy to forget over time. A couple of
concepts, such as exception handling and the
try
/catch
construction, are
also comparatively new in recent browsers. Scripters without formal
programming training tend to be rather loose in their attention to
detail in the error department—something that can come back to
bite you. On the other hand, the browser implementations of some of
the details of exception handling are far from compatible. If you
aren’t yet using exception-handling techniques in
your scripts (perhaps the needs of backward-compatibility prevent
it), you should still get to know the concepts. As time goes on and
the full W3C DOM becomes implemented in browsers, the notion of
“safe scripting” will include
regular application of exception-handling practices.
This chapter ends with some suggestions about improving script performance. Most scripts can scrape by with inefficiencies, but larger projects that deal with complex document trees and substantial amounts of hidden data delivered to the client must pay particular attention to performance. You’ll learn some practices here that you should apply even to short scripts.
Creating a JavaScript Variable
NN 2, IE 3
Problem
You want to create a JavaScript variable value either in the global space or privately within a function.
Solution
Use the var
keyword to define the first instance of
every variable, whether you assign a value to the variable
immediately or delay the assignment until later. Any variable defined
outside of a function is part of the global variable scope:
var myVar = someValue;
All script statements on the page, including those inside functions, have read/write access to a global variable.
When you define a variable with var
inside a
function, only statements inside the function can access that
variable:
function myFunction( ) { var myFuncVar = someValue; ... }
Statements outside of the function cannot reach the value of a variable whose scope is limited to its containing function.
Discussion
A JavaScript variable has no inherent limit to the amount of data it can hold. Maximum capacity is determined strictly by memory available to the browser application—information not accessible to your scripts.
Variable
scope is an important concept to understand in JavaScript. Not only
is a global variable accessible by all script statements in the
current window or frame, but statements in other frames or windows
(served from the same domain and server) can access those global
variables by way of the window or frame reference. For example, a
statement in a menu frame can reference a global variable named
myVar
in a frame named content
as follows:
parent.content.myVar
You don’t have to worry about the same global variable names colliding when they exist in other windows or frames, because the references to those variables will always be different.
Where you must exercise care is in defining a new variable inside a
function with the var
keyword. If you fail to use the
keyword inside the function, the variable is treated as a global
variable. If you have defined a global variable with the same name,
the function’s assignment statement overwrites the
value originally assigned to the global variable. The safest way to
avoid these kinds of problems is to always use the
var
keyword with the first instance of any
variable, regardless of where it’s located in your
scripts. Even though the keyword is optional for global variable
declarations, it is good coding style to use var
for globals as well. That way you can readily see where a variable is
first used in a script.
Although some programming languages distinguish between the tasks of declaring a variable (essentially reserving memory space for its value) and initializing a variable (stuffing a value into it), JavaScript’s dynamic memory allocation for variable values unburdens the scripter of memory concerns. A variable is truly variable in JavaScript in that not only can the value stored in the variable change with later reassignments of values, but even the data type of the variable’s value can change (not that this is necessarily good programming practice, but that’s simply a by-product of JavaScript’s loose data typing).
Speaking of good programming practice, it is generally advisable to define global variables near the top of the script, just as it’s also advisable to define heavily used variables inside a function at the top of the function. Even if you don’t have a value ready to assign to the variable, you can simply declare the variable as undefined with a statement like the following:
var myVar;
If you have multiple variables that you’d like to declare, you may do so compactly by separating the variable names with commas:
var myVar, counter, fred, i, j;
You may even combine declarations and initializations in a comma-delimited statement:
var myVar, counter = 0, fred, i, j;
In examples throughout this book, you typically find variables being
declared or initialized at the top of their scope regions, but not
always. It’s not unusual to find variables that are
about to be used inside a for
loop defined (with
their var
keywords) just before the loop
statements. For example, if a nested pair of loops is in the offing,
I may define the loop counter variables prior to the outer
loop’s start:
var i, j; for (i = 0; i < array1.length; i++) { for (j = 0; j < array1[i].array2.length; j++) { ... } }
This is merely my style preference. But in any case, this situation
definitely calls for declaring the variables outside of the loops for
another reason. If you were to use the var
keywords in the loop counter initialization statements (e.g.,
var
j
=
0;
), the nested loop would repeatedly invoke the
var
declaration keyword each time the outer loop
executes. Internally, the JavaScript interpreter creates a new
variable space for each var
keyword. Fortunately,
the interpreter is also able to keep track of which variable
repeatedly declared is the current one, but it places an unnecessary
burden on resources. Declare once, then initialize and reassign
values as often as needed. Thus, in complex functions that have two
or more outer for
loops, you should declare the
counter variable at the top of the function, and simply initialize
the value at the start of each loop.
As for selecting the names for your variables, there are some explicit rules and implicit customs to follow. The explicit rules are more important. A variable name cannot:
Begin with a numeral
Contain any spaces or other whitespace characters
Contain punctuation or symbols except the underscore character
Be surrounded by quote marks
Be a reserved ECMAScript keyword (see Appendix C)
Conventions among programmers with respect to devising names for variables are not rigid, nor do they affect the operation of your scripts. They do, however, help in readability and maintenance when it comes time to remember what your script does six months from now.
The main idea behind a variable name is to help you identify what kind of value the variable contains (in fact, names are commonly called identifiers). Littering your scripts with a bunch of one- or two-letter variables won’t help you track values or logic when reading the script. On the other hand, there are performance reasons (see Recipe 4.8) to keep names from getting outrageously long. The shorter the better, but not to the point of cryptic ciphers. If you need two or more words to describe the value, join the words together via underscore characters, or capitalize the first character of any words after the first word (a convention used throughout this book). Thus, either of the variable names in the following initializations are fine:
var teamMember = "George"; var team_member = "George";
Apply these rules and concepts to the identifiers you assign to
element name
and id
attributes,
as well. Your scripts will then have no trouble using these
identifiers in DOM object references.
Variable
names are case-sensitive. Therefore, it is
permissible (although not necessarily advisable) to reuse an
identifier with different case letters to carry different values. One
convention that you might employ is to determine which variables
won’t be changing their values during the execution
of your scripts (i.e., you will treat them as constants) and make
their names all uppercase. Netscape 6 and later implement a
forthcoming ECMAScript keyword called
const
, which you use in place of
var
to define a true constant value. No other
browser supports this keyword yet, so you can use variables as
constants and keep modification statements away from them.
JavaScript assigns data to a variable both “by reference” and “by value,” depending on the type of data. If the data is a true object of any kind (e.g., DOM object, array, custom object), the variable contains a “live” reference to the object. You may then use that variable as a substitute reference to the object:
var elem = document.getElementById("myTable"); var padWidth = elem.cellPadding;
But if the data is a simple value (string, number, Boolean), the
variable holds only a copy of the value, with no connection to the
object from which the value came. Therefore, the
padWidth
variable shown above simply holds a
string value; if you were to assign a new value to the variable, it
would have no impact on the table
element. To set
the object’s property, go back to the object
reference and assign a value to the property:
elem.cellPadding = "10";
If an object’s property value is itself another object, the variable receives that data as an object reference, still connected to its object:
var elem = document.getElementById("myTable"); var elemStyle = elem.style; elemStyle.fontSize = "18px";
Exercise care with DOM objects assigned to variables. It may seem as though the variable is a mere copy of the object reference, but changes you make to the variable value affect the document node tree.
See Also
Chapter 1, Chapter 2, and Chapter 3 discuss assigning values of different types—strings, numbers, arrays, and objects—to variables; Recipe 4.8 for the impact of variable name length on performance.
Creating a Named Function
NN 2, IE 3
Solution
For a function that receives no parameters, use the simple function declaration format:
function myFunctionName
( ) {
// statements go here
}
If the function is designed to receive parameters from the statement that invokes the function, define parameter variable names in the parentheses following the function name:
functionmyFunctionName
(paramVar1
,paramVar2
[, ...,paramVarN
]) { // statements go here }
You can define as many unique parameter variable identifiers as you
need. These variables become local variables inside the function
(var
declarations are implied). Following
JavaScript’s loosely typed conventions, parameter
variables may hold any valid data type, as determined by the
statement that invokes the function and passes the parameters.
Curly braces that contain the statements belonging to the function are required only when two or more statements are inside the function. It is good practice to use curly braces anyway, even for one-line statements, to assist in source code readability (a convention followed throughout this book).
The majority of long scripts throughout this book employ named functions, some with parameters, others without. Real-world examples abound, especially in recipes containing external JavaScript libraries, such as the DHTML API library in Recipe 13.3.
Discussion
A function is an object type in JavaScript, and the name you assign to the function becomes a case-sensitive identifier for that object. As a result, you cannot use a JavaScript-reserved keyword as a function name, nor should you use a function name that is also an identifier for one of your other global entities, such as variables or (in IE) element IDs. If you have two functions with the same name in a page, the one that comes last in source code order is the only available version. JavaScript does not implement the notion of function or method overloading found in languages such as Java (where an identically named method with a different number of parameter variables is treated as a separate method).
Invoke a function using parentheses:
myFunc( ); myFunc2("hello",42);
At times, you will need to assign a function’s reference to a property. For example, when you assign event handlers to element object properties (see Chapter 9), the assignment consists of a function reference. Such a reference is the name of the function but without parentheses, parameters, or quotes:
document.onclick = myFunc;
This kind of property assignment is merely setting the stage for a future invocation of the function.
Some programming languages distinguish between executable blocks of
code that operate on their own and those that return values. In
JavaScript, there is only one kind of
function. If the
function includes a return
statement, the function
returns a value; otherwise there is no returned value. Functions used
as what other environments might call subroutines commonly return
values simply because you define them to perform some kind of
information retrieval or calculation, and then return the result to
the statement that invoked the routine. When a function returns a
value, the call to the function evaluates to a value that can be
assigned immediately to a variable or be used as a parameter value to
some other function or method. Recipe 15.6 demonstrates this feature.
Its job is to display the part of the day (morning, afternoon, or
evening) in a welcome greeting that is written to the page as it
loads. A function called getDayPart(
)
(defined in the head portion of the
page) calculates the current time and returns a string with the
appropriate day part:
function dayPart( ) { var oneDate = new Date( ); var theHour = oneDate.getHours( ); if (theHour < 12) { return "morning"; } else if (theHour < 18) { return "afternoon"; } else { return "evening"; } }
That function is invoked as a parameter to the
document.write(
)
method that places the text in the
rendered page:
<script language="JavaScript" type="text/javascript"> <!-- document.write("Good " + dayPart( ) + " and welcome") //--> </script> <noscript>Welcome</noscript> to GiantCo.
It is not essential to pass the same number of arguments to a
function, as you have defined parameter variables for that function.
For example, if the function is called from two different places in
your script, and each place provides a different number of
parameters, you can access the parameter values in the function by
way of the arguments
property of the function
rather than by parameter variables:
function myFunc( ) { for (var i = 0; i < myFunc.arguments.length; i++) { // each entry in the arguments array is one parameter value // in the order in which they were passed } }
A typical function (except a nested function, as described in Recipe 4.3) exists in the global context of the window housing the current page. Just as with global variables, these global functions can be referenced by script statements in other windows and frames. See Section 7.0.1 in Chapter 7 for examples of referencing content in other frames.
How large a function should be is a matter of style. For ease of debugging and maintenance, it may be appropriate to divide a long function into sections that either branch out to subroutines that return values or operate in sequence from one function to the next. When you see that you use a series of statements two or more times within a large function, these statements are excellent candidates for removal to their own function that gets called repeatedly from the large function.
The other stylistic decision in your hands is where you place the curly braces. This book adopts the convention of starting a curly brace pair on the same line as the function name, and closing the pair at the same tab location as the function declaration. But you can place the opening curly brace on the line below the function name, and left-align it if you like:
function myFunc( ) { // statements go here }
Some coders feel this format makes it easier to keep brace pairs in sync. For a one-line function, the single statement can go on the same line as the function name:
function myFunc( ) {//statement goes here}
Adopt the style that makes the most logical sense to you and your code-reading eye.
See Also
Recipe 4.1 for a discussion about variables “by reference” and “by value”—a discussion that applies equally to function parameter variables; Recipe 4.3 for nesting functions.
Nesting Named Functions
NN 4, IE 4
Solution
Starting with IE 4 and NN 4, you can nest a function inside another function according to the following syntax model:
function myFuncA( ) { // statements go here function.myFuncB( ) { // more statements go here } }
In this construction, the nested function may be accessed only by statements in the outer function (but see the Discussion about Netscape 6 and later). Statements in the nested function have access to variables declared in the outer function, as well as to global variables. Statements in the outer function, however, do not have access to the inner function’s variables.
Discussion
The basic idea behind nested functions is that you can encapsulate all activity related to the outer function by keeping subroutine functions private to the outer function. Because the nested function is not directly exposed to the global space, you can reuse the function name in the global space or for a nested function inside some other outer function.
Netscape 6 and later extends the environment for nested functions in a logical and convenient way: a nested function acts as a method of the outer function object. There are some interesting ramifications with the way this feature is implemented. Consider the following test function set:
function myFuncA( ) { var valueA = "A"; myFuncB( ); function myFuncB( ) { var valueB = "B"; alert(valueB); alert(valueA); } }
When you invoke myFuncA( )
from the global space,
it invokes its nested function, which dutifully displays the values
of the two variables that are defined in the outer and inner
functions. IE behaves the same way, as expected. But in Netscape 6 or
later, you can invoke the nested function from the global space by
way of the outer function:
myFuncA.myFuncB( );
Because the outer function does not execute in this direct access to
myFuncB
, the valueA
variable is
not initialized with any value. When myFuncB( )
runs, it shows the value of valueB
, but
valueA
comes back as undefined
.
Therefore, access to the outer function’s variables
is possible only when the inner function is invoked by the outer
function.
See Also
Recipe 4.1 for a discussion of variable scope.
Creating an Anonymous Function
NN 4, IE 4
Problem
You want to define a function in the form of an expression that you can, for example, pass as a parameter to an object constructor or assign to an object’s method.
Solution
You can use an alternate syntax for defining functions without creating an explicitly named function (as shown in Recipe 4.2). Called an anonymous function, this syntax has all the components of a function definition except its identifier. The syntax model is as follows:
varsomeReference
= function( ) {statements go here
};
Statements inside the curly braces are semicolon-delimited JavaScript statements. You can define parameter variables if they’re needed:
varsomeReference
= function(paramVar1
[,...,paramVarN
]) {statements go here
};
Invoke the function via the reference to the function:
someReference
( );
Discussion
Anonymous function creation returns an object of type
function
. Therefore, you can assign the right side
of the statement to any assignment statement where a function
reference (the function name without parentheses) is expected. To
demonstrate, we’ll make a version of a shortcut
object constructor from Recipe 3.8. It starts with an ordinary
function definition that gets invoked as a method of four objects
defined with shortcut syntax:
function showAll( ) { alert("Employee " + this.name + " is " + this.age + " years old."); } var employeeDB = [{name:"Alice", age:23, show:showAll}, {name:"Fred", age:32, show:showAll}, {name:"Jean", age:28, show:showAll}, {name:"Steve", age:24, show:showAll}];
Notice how in the object constructors, a reference to the
showAll( )
function is assigned to the
show
method name. Invoking this method from one of
the objects is done in the following manner:
employeeDB[2].show( );
For the sake of example, we assign an anonymous function to the first
object. The anonymous function is custom-tailored for the first
object and replaces the reference to showAll( )
:
var employeeDB = [{name:"Alice", age:23, show :function( ) {alert("Alice\'s age is not open to the public.")}}, {name:"Fred", age:32, show:showAll}, {name:"Jean", age:28, show:showAll}, {name:"Steve", age:24, show:showAll}];
Now, if you invoke employeeDB[0].show( )
, the
special alert displays itself because the anonymous function is
running instead of the showAll( )
function. We
have saved the need to create an external function with its own
identifier just to act as an intermediary between the
show
method name and the statements to execute
when the method is invoked.
See Also
Recipe 4.2 for creating traditional named functions.
Delaying a Function Call
NN 2, IE 3
Solution
Use the window.setTimeout(
)
method to invoke a function once after a
delay of a number of milliseconds. You essentially set a timer to
trigger a function of your choice. The function is referenced as a
string, complete with parentheses, as in the following example:
var timeoutID = setTimeout("myFunc( )", 5000);
The method returns an ID for the time-out operation and should be
preserved in a global variable. If, at any time before the delayed
function fires, you wish to abort the timer, invoke the
clearTimeout( )
method with the time-out ID as the
parameter:
clearTimeout(timeoutID);
Once the timer is set, other script processing may proceed as usual,
so it is often a good idea to place the setTimeout(
)
call as the final statement of a
function.
Discussion
It’s important to understand what the
setTimeout( )
method doesn’t do:
it does not halt all processing in the manner of a delay that
suspends activity until a certain time. Instead, it simply sets an
internal countdown timer that executes the named function when the
timer reaches zero. For example, if you are creating a slide show
that should advance to another page after 15 seconds of inactivity
from the user, you would initially set the timer via the
onload
event handler for the page and the
resetTimer( )
function:
var timeoutID; function goNextPage( ) { location.href = "slide3.html"; } function resetTimer( ) { clearTimeout(timeoutID); timeoutID = setTimeout("goNextPage( )", 15000); }
You would also set an event handler for, say, the
onmousemove
event so that each time the user
activates the mouse, the autotimer resets to 15 seconds:
window.onmousemove = resetTimer;
The resetTimer( )
function automatically cancels
the previously set time-out before it triggers the
goNextPage( )
function, and then it starts a new
15-second timer.
If the function you are invoking via the delay requires parameters,
you can assemble a string with the values, even if those values are
in the form of variables within the function. But—and this is
important—the variable values cannot be object references.
Parameters must be in a form that will survive the conversion to the
string needed for the first argument of the setTimeout(
)
method. Recipe 8.4 demonstrates how you can convey names
of DOM form-related objects as ways of passing an object reference.
The tricky part is in keeping the quotes in order:
function isEMailAddr(elem) { var str = elem.value; var re = /^[\w-]+(\.[\w-]+)*@([\w-]+\.)+[a-zA-Z]{2,7}$/; if (!str.match(re)) { alert("Verify the e-mail address format."); setTimeout("focusElement('" + elem.form.name + "', '" + elem.name + "')", 0); return false; } else { return true; } }
In this example, the focusElement(
)
function requires two parameters that
are used to devise a valid reference to both a
form
object and a text input
object. Both parameters of the focusElement( )
function are strings. Because the first argument of
setTimeout( )
is, itself, a string, you have to
force the “stringness” of the
parameters to focusElement( )
by way of single
quotes placed within the extended string concatenation sequence. (The
zero milliseconds shown in the example is not a mistake for this
application. Learn why in the discussion for Recipe 8.4.)
If you are looking for a true delay between the execution of
statements within a function or sections of a function, JavaScript
has nothing comparable to commands available in some other
programming languages. But you can accomplish the same result by
dividing the original function into multiple functions—one
function for each section that is to run after a delay. Link the end
of one function to the next by ending each function with
setTimeout( )
, which invokes the next function in
sequence after the desired amount of time:
function mainFunc( ) { // initial statements here setTimeout("funcPart2( )", 10000); } function funcPart2( ) { // initial statements here setTimeout("finishFunc( )", 5000); } function finishFunc( ) { // final batch of statements here }
The related functions don’t have to be located adjacent to each other in the source code. If all related functions need to operate on the same set of values, you can cascade them as parameters (provided the parameters can be represented as nonobject values), or you can preserve them as global variables. If the values are related, it may be a good reason to define a custom object with values assigned to labeled properties of that object to make it easier to see at a glance what each function segment is doing with or to the values.
Another JavaScript method, setInterval(
)
, operates much like setTimeout(
)
, but repeatedly invokes the target function until told to
stop (via the clearInterval( )
method). The second
parameter (an integer in milliseconds) controls the amount of time
between calls to the target function.
See Also
Recipe 8.4 for using setTimeout( )
to keep script
execution synchronized; Recipe 10.11 for a sequence of three
initialization functions that are linked via calls to
setTimeout( )
to keep execution synchronicity in
order; Recipe 12.3 for an example of using a self-contained counter
variable in a repeatedly invoked function to execute itself a fixed
number of times; Recipe 13.9 and Recipe 13.10 for applications of
setInterval( )
in animation.
Branching Execution Based on Conditions
NN 4, IE 4
Problem
You want
your scripts to execute sections of code based on external values,
such as Booleans, user entries in text boxes, or user choices from
select
elements.
Solution
Use the
if
,
if
/else
,
or
switch
flow control constructions to
establish an execution path through a section of your scripts. When
you need to perform a special section of script if one condition is
met only, use the simple if
construction with a
conditional expression that tests for the condition:
if (condition
) { // statements to execute ifcondition
is true }
To perform one branch under one condition and another branch for all
other situations, use the
if
/else
construction:
if (condition
) { // statements to execute ifcondition
is true } else { // statements to execute ifcondition
is false }
You can be more explicit in the
else
clause by performing additional
condition tests:
if (conditionA
) { // statements to execute ifconditionA
is true } else if (conditionB
) { // statements to execute ofconditionA
is false andconditionB
is true } else { // statements to execute if bothconditionA
andconditionB
are false }
For multiple conditions, you should consider using the
switch
statement if the conditions are based on
string or numeric value equivalency:
switch (expression
) { casevalueA
: // statements to execute ifexpression
evaluates tovalueA
break; casevalueB
: // statements to execute ifexpression
evaluates tovalueB
break; ... default: // statements to execute ifexpression
evaluates to no case value }
The break
statements in each of the
case
branches assure that the
default
branch (which is optional) does not also
execute.
Discussion
A condition expression in the if
and
if
/else
constructions is an
expression that evaluates to a Boolean true
or
false
. Typically, such expressions use
comparison operators (=
=
, = = =
, !=
,
!= =
, <
,
<=
, >
,
>=
) to compare the relationship between two
values. Most of the time, you are comparing a variable value against
some constant or known value:
var theMonth = myDateObj.getMonth( ); if (theMonth = = 1) { // zero-based value means the date is in February monLength = getLeapMonthLength(myDateObj); } else { monLength = getMonthLength(theMonth); }
JavaScript offers some additional shortcut evaluations for condition
expressions. These shortcuts come in handy when you need to branch
based on the existence of an object or property. Table 4-1 lists the conditions that automatically
evaluate to true
or false
when
placed inside the parentheses of a condition expression. For example,
the existence of an object evaluates to true
,
which allows a construction such as the following to work:
if (myObj) { // myObj exists, so use it }
true |
false |
String has one or more characters |
Empty string |
Number other than zero |
|
Nonnull value |
|
Referenced object exists |
Referenced object does not exist |
Object property is defined and evaluates to a string of one or more characters or a nonzero number |
Object property is undefined, or its value is an empty string or zero |
When testing for the existence of an object property (including a
property of the global window
object), be sure to
start the reference with the object, as in the following:
if (window.innerHeight) { ... }
But you also need to be careful when testing for the existence of a
property if there is a chance that its value could be an empty string
or zero. Such values force the conditional expression to evaluate to
false
, even though the property exists. Therefore,
it is better to test for the data type of the property with the
typeof
operator. If you’re not
sure about the data type, test the data type against the
undefined
constant:
if (typeof myObj.myProperty != "undefined" ) { // myProperty exists and has a value of some kind assigned to it }
If there is a chance that neither the object nor its property exists, you need to group together conditional expressions that test for the existence of both. Do this by testing for the object first, then the property. If the object does not exist, the expression short-circuits the test of the property:
if (myObj && typeof myObj.myProperty != "undefined") { // myObj exists, and so does myProperty }
If you test for the property first, the test fails with a script error because the expression with the object fails unceremoniously.
JavaScript also provides a shortcut syntax that lets you avoid the curly braces for simple assignment statements that execute differently based on a condition. The syntax is as follows:
var myValue = (condition
) ?value1
:value2
;
If the condition evaluates to true
, the righthand
expression evaluates to the first value; otherwise, it evaluates to
the second value. For example:
var backColor = (temperature > 100) ? "red" : "blue";
Several recipes in later chapters use this shortcut construction frequently, even to two levels deep. For example:
var backColor = (temperature > 100) ? "red" : ((temperature < 80) ? "blue" : "yellow");
This shortcut expression is the same as the longer, more readable, but less elegant version:
var backColor ; if (temperature > 100) { backColor = "red"; } else if (temperature < 80) { backColor = "blue"; } else { backColor = "yellow"; }
When you have lots of potential execution branches, and the triggers
for the various branches are not conditional expressions per se, but
rather the value of an expression, then the switch
construction is the way to go. In the following example, a form
contains a select
element that lets a user choose
a size for a product. Upon making that choice, an
onchange
event handler in the
select
element triggers a function that inserts
the corresponding price for the size in a text box:
function setPrice(form) { var sizeList = form.sizePicker; var chosenItem = sizeList.options[sizeList.selectedIndex].value; switch (chosenItem) { case "small" : form.price.value = "44.95"; break; case "medium" : form.price.value = "54.95"; break; case "large" : form.price.value = "64.95"; break; default: form.price.value = "Select size"; } }
If the switch
expression always evaluates to one
of the cases, you can omit the default
branch, but
while you are in development of the page, you might leave it there as
a safety valve to alert you of possible errors if the expression
should evaluate to an unexpected value.
See Also
Most of the recipes in Chapter 15 use the shortcut conditional statement to equalize disparate event models.
Handling Script Errors Gracefully
NN 6, IE 5
Problem
You want to process all script errors out of view of users, and thus prevent the browser from reporting errors to the user.
Solution
The quick-and-dirty, backward-compatible way to prevent runtime script errors from showing themselves to users is to include the following statements in a script within the head portion of a page:
function doNothing( ) {return true;} window.onerror = doNothing;
This won’t stop compile-time script errors (e.g., syntax errors that the interpreter discovers as the page loads). It also won’t reveal to you, the programmer, where errors lurk in your code. Add this only if you need to deploy a page before you have fully debugged the code; remove it to test your code.
In IE 5 or later and Netscape 6 or later, you can use more formal
error (exception) handling. To prevent earlier
browsers from tripping up on
the specialized syntax used for this type of processing, embed these
statements in <script>
tags that specify
JavaScript 1.5 as the language attribute
(language="JavaScript1.5"
).
Wrap statements that might cause (throw) an exception in a
try
/catch
construction. The statement to execute goes into the
try
section, while the catch
section processes any exception that occurs:
<script type="text/javascript" language="JavaScript1.5"> function myFunc( ) { try { // statement(s) that could throw an error if various conditions aren't right } catch(e) { // statements that handle the exception (error object passed to e variable) } } </script>
Even if you do nothing in the catch
section, the
exception in the try
section is not fatal.
Subsequent processing in the function, if any, goes on, provided it
is not dependent upon values created in the try
section. Or, you can bypass further processing in the function and
gracefully exit by executing a return
statement
inside the catch
section.
Discussion
Each thrown exception generates an instance of the JavaScript
Error
object. A reference to this object
reaches the catch
portion of a
try
/catch
construction as a
parameter to the catch
clause. Script statements
inside the catch
clause may examine properties of
the object to learn more about the nature of the error. Only a couple
of properties are officially sanctioned in the ECMAScript standard so
far, but some browsers implement additional properties that contain
the same kind of information you see in script error messages. Table 4-2 lists informative Error
object properties and their browser support.
Error messages are never intended to be seen by users. Use the
description
or message
property
of an error object in your own exception handling to decide how to
process the exception. Unfortunately, the precise message from the
various browsers is not always identical for a given error. For
example, if you try to reference an undefined object, IE reports the
description string as:
'myObject' is undefined
Netscape 6 or later, on the other hand, reports:
myObject is not defined
This makes cross-browser exception handling a bit difficult. In this case, you could try to fudge it by performing string lookups (regular expression matches) for the object reference and the fragment “defined” as in the following:
try { window.onmouseover = trackPosition; } catch(e) { var msg = (e.message) ? e.message : e.description; if (/trackPosition/.exec(msg) && /defined/.exec(msg)) { // trackPosition function does not exist within page scope } }
You can also intentionally throw an exception as a way to build
exception handling into your own processing. The following function
is a variation of a form validation function that tests for the entry
of only a number in a text box. The try
clause
tests for an incorrect value. If found, the clause creates its own
instance of an Error
object and uses the
throw
method to trigger an exception. Of course,
the thrown exception is immediately caught by the following
catch
clause, which displays the alert message and
refocuses the text box in question:
function processNumber(inputField) { try { var inpVal = parseInt(inputField.value, 10); if (isNaN(inpVal)) { var msg = "Please enter a number only."; var err = new Error(msg); if (!err.message) { err.message = msg; } throw err; } // it's safe to process number here } catch (e) { alert(e.message); inputField.focus( ); inputField.select( ); } }
This kind of function is invoked by both an
onchange
event handler for the text field and a
batch validation routine, as described in Chapter 8.
Improving Script Performance
NN 2, IE 3
Solution
When swallowing small doses of code, JavaScript interpreters tend to process data speedily. But if you throw a ton of complex and deeply nested code at a browser, you may notice some latency, even if all the data is downloaded in the browser.
Here are a handful of useful tips to help you unclog potential processing bottlenecks in your code:
Look for these culprits especially inside loops, where delays become magnified.
Discussion
One of the most inefficient functions in the JavaScript language is
eval( )
. This function converts a string
representation of an object to a genuine object reference. It becomes
a common crutch when you find yourself with a string of an
object’s name or ID, and need to build a reference
to the actual object. For example, if you have a sequence of mouse
rollover images comprising a menu, and their names are
menuImg1
, menuImg2
, and so on,
you might be tempted to create a function that restores all images to
their normal image with the following construction:
for (var i = 0; i < 6; i++) { var imgObj = eval("document.menuImg" + i); imgObj.src = "images/menuImg" + i + "_normal.jpg"; }
The temptation is there because you are also using string
concatenation to assemble the URL of the associated image file.
Unfortunately, the eval( )
function in this loop
is very wasteful.
When it comes to referencing element objects, there is almost always
a way to get from a string reference to the actual object reference
without using the eval( )
function. In the case of
images, the document.images
collection (array)
provides the avenue. Here is the revised, more streamlined loop:
for (var i = 0; i < 6; i++) { var imgObj = document.images["menuImg" + i]; imgObj.src = "images/menuImg" + i + "_normal.jpg"; }
If an element object has a name or ID, you can reach it through some
collection that contains that element. The W3C DOM syntax for
document.getElementById( )
is a natural choice
when working in browsers that support the syntax and you have the
element’s ID as a string. But even for older code
that supports names of things like images and form controls, there
are collections to use, such as document.images
and the elements
collection of a
form
object
(document.myForm.elements["elementName"]
). For
custom objects, see the later discussion about simulated hash tables.
Hunt down every eval( )
function in your code and
find a suitable, speedier replacement.
Another performance grabber is the with
construction. The purpose of this control statement is to help narrow
the scope of statements within a block. For example, if you have a
series of statements that work primarily with a single
object’s properties and/or methods, you can limit
the scope of the block so that the statements assume properties and
methods belong to that object. In the following script fragment, the
statements inside the block invoke the sort( )
method of an array and read the array’s
length
property:
with myArray { sort( ); var howMany = length; }
Yes, it may look efficient, but the interpreter goes to extra lengths to fill in the object references before evaluating the nested expressions. Don’t use this construction.
It takes processing cycles to evaluate any expression or reference. The more “dots” in a reference, the longer it takes to evaluate the reference. Therefore, you want to avoid repeating a lengthy object reference or expression if it isn’t necessary, especially inside a loop. Here is fragment that may look familiar to you from your own coding experience:
function myFunction(elemID) { for (i = 0; i < document.getElementById(elemID).childNodes.length; i++) { if (document.getElementById(elemID).childNodes[i].nodeType = = 1) { // processing element nodes here } } }
In the course of this function’s execution, the
expression document.getElementById(
)
evaluates twice as many times as there
are child nodes in the element whose ID is passed to the function. At
each start of the for
loop’s
execution, the limit expression evaluates the method; then the nested
if
condition evaluates the same expression each
time through the loop. More than likely, additional statements in the
loop evaluate that expression to access a child node of the outer
element object. This is very wasteful of processing time.
Instead, at the cost of one local variable, you can eliminate all of this repetitive expression evaluation. Evaluate the unchanging part just once, and then use the variable reference as a substitute thereafter:
function myFunction(elemID) { var elem = document.getElementById(elemID); for (i = 0; i < elem .childNodes.length; i++) { if (elem .childNodes[i].nodeType = = 1) { // processing element nodes here } } }
If all of the processing inside the loop is with only child nodes of the outer loop, you can further compact the expression evaluations:
function myFunction(elemID) { var elemNodes = document.getElementById(elemID).childNodes; for (i = 0; i < elemNodes.length; i++) { if (elemNodes[i].nodeType = = 1) { // processing element nodes here } } }
As an added bonus, you have also reduced the source code size. If you find instances of repetitive expressions whose values don’t change during the course of the affected script segment, consider them candidates for pre-assignment to a local variable.
Next, eliminate time-consuming iterations through arrays, especially multidimensional arrays or arrays of objects. If you have a large array (say, more than about 100 entries), even the average lookup time may be noticeable. Instead, use the techniques shown in Recipe 3.9 to perform a one-time generation of a simulated hash table of the array. Assemble the hash table while the page loads so that any delay caused by creating the table is blended into the overall page-loading time. Thereafter, lookups into the array will be nearly instantaneous, even if the item found is the last item in the many-hundred member array.
The final tip addresses use of the document.write(
)
method to generate content while the page loads. Treat
this method as if it were an inherently slow input/output type of
operation. Invoke the method as infrequently as possible. If you are
writing a lot of content to the page, accumulate the HTML into a
string variable, and blast it to the page with one call to
document.write( )
.
See Also
Recipe 3.9 for details on creating a simulated hash table from an
array; Recipe 3.13 for a rare case where the eval(
)
function can’t be avoided.
Get JavaScript & DHTML Cookbook 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.