Introduction
Sometimes a JavaScript-enhanced web page needs just a little code to add a nice little feature. But far more often the JavaScript features are significant enough to warrent using one or more libraries to provide a higher-level infrastructure than that provided by the JavaScript browsers alone. For example, in our course, we use the Core library developed and presented in the Yank and Adams Simply JavaScript book, which provides two different types of infrastructure for our JavaScript-enhanced applications:
- The Core library provides a browser-independent way to manage event handlers, Core.addEventListener() and some related functions (Core.removeEventListener(), Core.preventDefault(), and Core.stopPropagation(), and Core.Start()). These functions are needed because Internet Explorer has traditionally used a different event management model from all other browsers; using these functions means your code never has to deal with these browser differences.
- The Core library also provides some very useful functions that simply are missing from the JavaScript specification. They let you used DOM element classes as easily as document.getElementById() and document.getElementsByTagName() let you work with element ids and tag names.
There are several popular JavaScript libraries in wide-spread use: jQuery, Prototype, and the Yahoo! User Interface Library (YUI), for example. These libraries provide overlapping functionalities, but it is perfectly reasonable to use more than one of the in a single web application. In addition to major libraries, there are also lots of “snippets” of JavaScript that get added to web pages that provide useful features: perhaps ones that you wrote yourself for an earlier project; perhaps some that a previous developer working on a client’s site produced; perhap some “neat” effect from another site that seems perfect for the one you are working on.
The point is that real-world JavaScript programming almost always requires your code to “play nice” with someone else’s code that is going to be loaded into the browser along with yours: code that you did not write and code that you might not have the time or inclination to study just to find out what it does and/or how it does it.
There are two issues that have to be dealt with:
-
The first one is global variables. In JavaScript, any variable that is defined outside of any function or
object can be referenced from within any function or object. The same holds true for variables that are
defined inside a function or object unless they are declared using the var
keyword. The problem here is that you might pick the same name for one of your global variables that
“the other guy” chose for one of hers, a recipe for obscure bugs plaguing both your code and the
other code you are sharing the browser with. Global variables must be eliminated or at least reduced to an
absolute minimum.
The problem is that you need to be able to share variables among the various functions in your program, and the option of passing everything from one function to another as a big set of parameters is messy, error-prone, and inefficient. (People have tried it.) But sharing a well-documented set of application-specific variables is actually a good thing. The goal is to set up your application so that it can have access to its own set of shared variables without conflicting with the browser-wide global variables.
Global variables are actually properties of the global object that the browser creates, the window object.
The major libraries hide their library-specific global variables as much as possible. For example, if you use the Yahoo Interface Library, it will create exactly one global variable, named YAHOO, and then put all its globals in that object. (Techncially, the name is window.YAHOO, but for browser-wide globals, you can omit the name of the containing object.) If you use the Core library from the Yank and Adams book, it will create exactly one global variable, named Core. If you use jQuery, it will create three global variables, named jQuery, _jQuery, and $. The Prototype global variable is named Prototype, but it also uses a global variable named $. (Note the name conflict; there are way to deal with it, but it cannot be ignored if you want to use both libraries.)
Furthermore, browsers use the global object to hold all sorts of internal houskeeping information. (I just looked at the window object in the browser that I have open right now and found 100 global variables there.) It’s a minefield trying to avoid interfering with other libraries and the browser’s own global variables.
-
The second issue is similar to the first one in that it also deals with different programs interfering with
each other. But in this case, the issue is the DOM tree: the browser creates a JavaScript tree of objects
using the global variable window.document (again, the
“window.” part is usually omitted when talking about it). By extension, all the nodes in the DOM
tree are global variables too: any piece of JavaScript code can get access to any node in the DOM tree by
calling document.getElementByTagName() or various other techniques. The
issue here is event handlers, which are references to functions that get attached to well-known global
variables within DOM tree nodes. For example, if you write a function to validate the data a user enters on
a form, you can put a reference to your function in the the form object’s submit property. The problem with this is that since the DOM is global, some
other JavaScript code could do the same thing. Depending which code gets initialized first, either setting
up your code’s submit event handler could destroy another program’s handler, or another program
could destroy the reference to your submit event handler when it sets up its own.
The solution to this problem is simple: use the event listener mechanism instead of the event handler mechanism. A node in the DOM tree can have just one handler for a particular type of event, but there can be multiple listeners: the browser keeps an internal list of all listener functions for a node’s events and calls them all when the event triggers. There is more overhead associated with listeners compared to handlers, but it is trivial compared to the impact of code that doesn’t work! (Event handlers are sometimes safe to use: for example, if you use JavaScript to create a node and add it to the DOM tree, it is highly unlikely that some other JavaScript code is going to find it, and you can safely use handlers for the events associated with that node.)
The remainder of this document goes through the process of setting up a JavaScript application that creates no global variables (properties of the global window object), but which has any number of application-global variables and functions. For this example, the application will use the Core library from the Yank and Adams book for setting up event listeners.
Getting Started
All JavaScript applications go through two stages: first some initialization code is run to get the application ready to start handling events, then the application reacts to events that occur. That is, the initialization code sets up event listeners and application-global variables, then the event listeners get called by the browser when events, such as mouse clicks and keyboard actions, happen.
The Core library defines a function, Core.start() that can be used to set up your application’s initialization code as soon as the browser has finished building the DOM tree for the web page. Since your application will include functions that respond to events, your initialization code needs access to the complete DOM tree in order to be able to attach your event listeners to the desired DOM elements. This implies that your initialization function may be defined as soon as your JavaScript code file is processed by the browser, but it must not actually execute until the browser has completed building the DOM Tree.
The Core library defines a function named Core.start() to handle getting your initialization function to run at the right time. (It makes your initialization function an event listener for the window.onload event.) Core.start() requires you to name your initialization function init(), and it requires your init() function to be defined inside an object; you pass that object to Core.start(). For example, assuming a web page has already loaded core.js, and except for the fact that it creates a global varible named myObject, this would work:
var myObject = {}; myObject.init = function() { /* initialize your application here */ }; Core.start( myObject );
Equivalently:
var myObject = { init: function() { /* initialize your application here */ }; Core.start( myObject );
You can eliminate the global variable quite easily:
Core.start( { init: function() { /* initialize your application here */ } } );
What Core.start() does is to install your init() function as an event listener for the window.onload event, which the browser automatically triggers when it finishes building the DOM tree for the web page.
By the way, if you had a big application that you divided into separate modules, you could initialize the modules separately by calling Core.start() multiple times. Because the init() functions are event listeners rather than event handlers, they don’t interfere with each other, and because they are defined inside different objects, their names do not conflict with each other either:
Core.start( { init: function() { /* initialize the one module of your application here */ } }); Core.start( { init: function() { /* initialize the another module of your application here */ } });
Real Applications Have Event Listeners
The previous section is a good start, but the functions in real applications have more than just comments in them! And that’s where things start to get complicated.
Let’s build an application that actually has an event listener. Now there have to be two functions: init() and the event listener, which we will call myListener(). To be realistic, the application will also need to connect the listener to some event arising from some node in the DOM tree. For an example, here is a web page with a button on it; the goal is to attach an event listener to click events on the button:
<html> <head> <title> My Page </title> <script type="text/javascript" src="scripts/core.js"></script> <script type="text/javascript" src="scripts/myscript.js"></script> </head> <body> <button id="myButton" /> </body> </html>
We would like to set this up by creating an object like this to pass to Core.start:
{ myListener: function(evt) { console.log("myListener"); }, init: function() { Core.addEventListener(theButton, "click", myListener); } }
Aside from the fact that theButton isn’t defined yet, this code does not work because the variable named myListener is not in scope inside function init(). You might think it would be since both are defined inside the same object. But it’s not: inside a function, a simple variable name refers to a local variable defined inside that function or (since JavaScript allows functions to be defined inside function) to a variable defined in a containing function (or a containing function’s containing function …). Finally, if the search through all level of enclosing functions fails, JavaScript will look for the variable in the global object, window. It’s as if the previous example had been written:
{ myListener: function(evt) { console.log("myListener"); }, init: function() { Core.addEventListener(theButton, "click", window.myListener); } }
Here is a solution that works but has problems:
var myObject = { myListener: function(evt) { console.log("myListener"); }, init: function() { Core.addEventListener(theButton, "click", myObject.myListener); } }; Core.start( myObject );
The problems are:
- The global variable myObject can’t be eliminated the way we did earlier because it has to be used to reference the object from within init().
- If we try to encapsulate our applicaton inside the named object, myObject, every reference to a shared variable or function name in the whole application has to be prefaced with myObject.—a major typing nuisance at best, and a likely source of hard to locate bugs under normal circumstances.
What we really want to do is to wrap a function around our entire application so that JavaScript will find all our application’s function and variable names before it “escapes” to the global object, window.
This works (except theButton is still not defined):
function myWrapperFunction() { function myListener(evt) { console.log("myListener"); } var myObject = { init: function() { Core.addEventListener(theButton, "click", myListener); } } return myObject; } Core.start( myWrapperFunction() );
To get the argument to pass to Core.start() (an object containing a function named init()), we call myWrapperFunction(), which returns myObject, which JavaScript passes as the argument to Core.start().
We’ve eliminated the need to qualify all the references to our shared variables and functions with an object name, and we eliminated using a global variable for the object that gets passed to Core.start()—but we’ve now introduced a different global variable: myWrapperFunction. Sigh.
But wait! JavaScript allows you to define functions with no name. Just define the function inside parentheses:
(function() { console.log("Hello, my name is"); })
That function definition is pretty useless because there is no name you could use to call the function. BUT, you can call it when you define it the way you call any function, by putting a pair of parentheses after it:
(function() { console.log("Hello, my name is"); })()
Note that we are playing loose with semicolons here: the previous two examples look like expressions (part a statement) because they do not end with semicolons. But JavaScript does not actually require a semicolon at the end of a statement. If it has a complete statement when it gets to the end of a line, it will consider that the end of a statement and go on to the next line. So this code gives an error:
(function() { console.log("Hello, my name is"); }) ()
In this case, JavaScript will not know what to do with the parentheses on the second line and will generate a syntax error. Adding a semicolon after them doesn’t do any good: the first line could be a complete statement and nothing you can do will convince JavaScript otherwise. This problem shows up in return statements because a return value is optional. For example:
return {}
Here, the first statement is treated as a complete return statement with no return value, and the second line is simply the definition of an empty object that is not saved, sort of like defining an anonymous function but not calling it. We will come back to this digression in the last code sample. For now, we have a mechanism for creating a namespace for our application without creating any global variables. Just replace myWrapperFunction() with an anonymous self-executing function and use it as the argument to Core.start():
Core.start( ( function() /* start of anonymous self-executing function * { /* start of our application's namespace */ function myListener(evt) { console.log("myListener"); } var myObject = { init: function() { Core.addEventListener(theButton, "click", myListener); } } return myObject; } /* end of our application's namespace */ )() /* end of the anonymous self-executing function and its invocation */ ); /* end of the call to Core.start()
To make it work, we need to define and initialize theButton. If we need to reference it from other parts of the application it would look like this:
Core.start( ( function() /* start of anonymous self-executing function * { /* start of our application's namespace */ function myListener(evt) { console.log("myListener"); } var theButton = null; var myObject = { init: function() { theButton = document.getElementsByTagName('button')[0]; Core.addEventListener(theButton, "click", myListener); } } return myObject; } /* end of our application's namespace */ )() /* end of the anonymous self-executing function and its invocation */ ); /* end of the call to Core.start()
If the only refernce to theButton is from within init(), good programming practice says to make it local to that function and keep it out of the application’s “global” namespace:
Core.start( ( function() /* start of anonymous self-executing function * { /* start of our application's namespace */ function myListener(evt) { console.log("myListener"); } /* theButton declaration removed from here */ var myObject = { init: function() { var theButton = document.getElementsByTagName('button')[0]; Core.addEventListener(theButton, "click", myListener); } } return myObject; } /* end of our application's namespace */ )() /* end of the anonymous self-executing function and its invocation */ ); /* end of the call to Core.start()
Or eliminate it entirely. (This is arguably harder code to read, so it’s up to the programmer to decide whether this or the previous example is “better.”):
Core.start( ( function() /* start of anonymous self-executing function * { /* start of our application's namespace */ function myListener(evt) { console.log("myListener"); } var myObject = { init: function() { /* theButton initialization statement removed from here */ Core.addEventListener(document.getElementsByTagName('button')[0], "click", myListener); } } return myObject; } /* end of our application's namespace */ )() /* end of the anonymous self-executing function and its invocation */ ); /* end of the call to Core.start()
As an added touch, we can eliminate the application-global variable, myObject, being careful not to trigger the return-without-semicolon issue mentioned above:
Core.start( ( function() /* start of anonymous self-executing function * { /* start of our application's namespace */ function myListener(evt) { console.log("myListener"); } return { init: function() { Core.addEventListener(document.getElementsByTagName('button')[0], "click", myListener); } }; /* semicolon added here */ /* return statement removed from here */ } /* end of our application's namespace */ )() /* end of the anonymous self-executing function and its invocation */ ); /* end of the call to Core.start()
A final note: when a function returns, all its local variables cease to exist. New copies of them will be created if the function is executed again, but the original set is lost when the function returns. In a language like C or C++, a function that returns a reference to a local variable is an error: the reference is to a part of memory that has been freed up for other uses. But our application framework returns a reference to a local variable (the object containing the init() method), yet it works because JavaScript implements what is called a closure: for so long as any part of that object and its environment exists, the entire environment is preserved. For example, in the version of our example where theButton was defined in the scope of the anonymous self-executing function (an “application-global” variable), it would still continue to be accessible to our event handler long after the self-executing function had returned. Closures are supported by other programming languages (Lisp, Python, and Ruby are notable examples), but “don’t try this in C!”
Summary
This page has shown how to set up a JavaScript application so that it has its own space in which to name shared variables and functions without creating any global variables. Although the example is for an application that uses the Core library of the Yank and Adams book, the same technique adapts easily to work with any other library.
JavaScript’s nested functions, self-executing anonymous functions, and closures all work together to provide the desired environment for an application to work in. The one drawback is that syntax errors can be hard to find: once braces and/or parentheses become unbalanced, the framework’s code:
Core.start((function(){<your code goes here>return{init:function(){<and here>}};})());
can make mistakes difficult to locate. To deal with this problem, add code to the framework incrementally, and immediately type closing braces and parentheses right after you type in an opening one, then go back and fill in the middle.