Section 10.1. Creating Modules and Namespaces


10.1. Creating Modules and Namespaces

If you want to write a module of JavaScript code that can be used by any script and can be used with any other module, the most important rule you must follow is to avoid defining global variables. Anytime you define a global variable, you run the risk of having that variable overwritten by another module or by the programmer who is using your module. The solution instead is to define all the methods and properties for your module inside a namespace that you create specifically for the module.

JavaScript does not have any specific language support for namespaces, but JavaScript objects work quite well for this purpose. Consider the provides( ) and defineClass( ) utility methods developed in Examples 9-8 and 9-10, respectively. Both method names are global symbols. If you want to create a module of functions for working with JavaScript classes, you don't define these methods in the global namespace. Instead, write code like this:

 // Create an empty object as our namespace // This single global symbol will hold all of our other symbols var Class = {}; // Define functions within the namespace Class.define = function(data) { /* code goes here */ } Class.provides = function(o, c) { /* code goes here */ } 

Note that you're not defining instance methods (or even static methods) of a JavaScript class here. You are defining ordinary functions and storing references to them within a specially created object instead of in the global object.

This code demonstrates the first rule of JavaScript modules: a module should never add more than a single symbol to the global namespace. Here are two suggestions that are good common-sense adjuncts to this rule:

  • If a module adds a symbol to the global namespace, its documentation should clearly state what that symbol is.

  • If a module adds a symbol to the global namespace, there should be a clear relationship between the name of that symbol and the name of the file from which the module is loaded.

In the case of the class module, you can put the code in a file named Class.js and begin that file with a comment that looks like this:

 /**  * Class.js: A module of utility functions for working with classes.  *  *   This module defines a single global symbol named "Class".  *   Class refers to a namespace object, and all utility functions  *   are stored as properties of this namespace.  **/ 

Classes are pretty important in JavaScript, and there is certain to be more than one useful module for working with them. What happens if two modules both use the global symbol Class to refer to their namespace? If this happens, you're back where you started, with a namespace collision. By using a namespace you've reduced the likelihood of a collision but have not eliminated it entirely. Following a filenaming convention helps a lot. If two conflicting modules are both named Class.js, they cannot be stored in the same directory. The only way that a script could include two different Class.js files is if they were stored in different directories, such as utilities/Class.js and flanagan/Class.js.

And if scripts are stored in subdirectories, the subdirectory names should probably be part of the module name. That is, the "Class" module defined here should really be called flanagan.Class. Here's what the code might look like:

 /**  * flanagan/Class.js: A module of utility functions for working with classes.  *  *   This module creates a single global symbol named "flanagan" if it  *   does not already exist. It then creates a namespace object and stores  *   it in the Class property of the flanagan object.  All utility functions  *   are placed in the flanagan.Class namespace.  **/ var flanagan;                  // Declare a single global symbol "flanagan" if (!flanagan) flanagan = {};  // If undefined, make it an object flanagan.Class = {}            // Now create the flanagan.Class namespace // Now populate the namespace with our utility methods flanagan.Class.define = function(data) { /* code here */ }; flanagan.Class.provides = function(o, c) { /* code here */ }; 

In this code, the global flanagan object is a namespace for namespaces. If I wrote another module to hold utility functions for working with dates, I could store those utilities in the flanagan.Date namespace. Notice that this code declares the global symbol flanagan with a var statement before testing for the presence of that symbol. This is because attempting to read an undeclared global symbol throws an exception, whereas attempting to read a declared but undefined symbol simply returns the undefined value. This is a special behavior of the global object only. If you attempt to read a nonexistent property of a namespace object, you simply get the undefined value with no exception.

With a two-level namespace like flanagan.Class, we now seem pretty safe from name collisions. If some other JavaScript developer whose last name is Flanagan decides to write a module of class-related utilities, a programmer who wanted to use both modules would find herself in trouble. But this seems pretty unlikely. For complete certainty, however, you can adopt a convention from the Java programming language for globally unique package name prefixes: start with the name of an Internet domain that you own. Reverse it so that the top-level domain (.com, or whatever) comes first, and use this as the prefix for all your JavaScript modules. Since my web site is at davidflanagan.com, I would store my modules in the file com/davidflanagan/Class.js and use the namespace com.davidflanagan.Class. If all JavaScript developers follow this convention, no one else will define anything in the com.davidflanagan namespace because no one else owns the davidflanagan.com domain.

This convention may be overkill for most JavaScript modules, and you don't need to follow it yourself. But you should be aware of it. Don't accidentally create namespaces that might be someone else's domain name: never define a namespace using a reversed domain name unless you own the domain yourself.

Example 10-1 demonstrates the creation of a com.davidflanagan.Class namespace. It adds error checking that was missing from the previous examples and throws an exception if com.davidflanagan.Class already exists, or if com or com.davidflanagan already exists but does not refer to an object. It also demonstrates that you can create and populate a namespace with a single object literal rather than doing so in separate steps.

Example 10-1. Creating a namespace based on a domain name

 // Create the global symbol "com" if it doesn't exist // Throw an error if it does exist but is not an object var com; if (!com) com = {}; else if (typeof com != "object")     throw new Error("com already exists and is not an object"); // Repeat the creation and type-checking code for the next level if (!com.davidflanagan) com.davidflanagan = {} else if (typeof com.davidflanagan != "object")     throw new Error("com.davidflanagan already exists and is not an object"); // Throw an error if com.davidflanagan.Class already exists if (com.davidflanagan.Class)     throw new Error("com.davidflanagan.Class already exists"); // Otherwise, create and populate the namespace with one big object literal com.davidflanagan.Class = {     define: function(data) { /* code here */ },     provides: function(o, c) { /* code here */ } }; 

10.1.1. Testing the Availability of a Module

If you are writing code that depends on an external module, you can test for the presence of that module simply by checking for its namespace. The code for doing this is a little tricky because it requires you to test for each component of the namespace. Notice that this code declares the global symbol com before testing for its presence. You have to do this just as you do when defining the namespace:

 var com;  // Declare global symbol before testing for its presence if (!com || !com.davidflanagan || !com.davidflanagan.Class)     throw new Error("com/davidflanagan/Class.js has not been loaded"); 

If module authors follow a consistent versioning convention, such as making the version number of a module available through the VERSION property of the module's namespace, it is possible to test for the presence not just of the module, but of a specific version of the module. At the end of this chapter, I'll provide an example that does just this.

10.1.2. Classes as Modules

The "Class" module used in Example 10-1 is simply a cooperating set of utility functions. There is no restriction on what a module may be, however. It might be a single function, a JavaScript class, or even a set of cooperating classes and functions.

Example 10-2 shows code that creates a module consisting of a single class. The module relies on our hypothetical Class module and its define( ) function. (See Example 9-10 if you've forgotten how this utility function works.)

Example 10-2. A complex-number class as a module

 /**  * com/davidflanagan/Complex.js: a class representing complex numbers  *  * This module defines the constructor function com.davidflanagan.Complex( )  * This module requires the com/davidflanagan/Class.js module  **/ // First, check for the Class module var com;  // Declare global symbol before testing for its presence if (!com || !com.davidflanagan || !com.davidflanagan.Class)     throw new Error("com/davidflanagan/Class.js has not been loaded"); // We know from this test that the com.davidflanagan namespace // exists, so we don't have to create it here.  We'll just define // our Complex class within it com.davidflanagan.Complex = com.davidflanagan.Class.define({     name: "Complex",     construct: function(x,y) { this.x = x; this.y = y; },     methods: {         add: function(c) {             return new com.davidflanagan.Complex(this.x + c.x,                                                  this.y + c.y);         }     }, }); 

You can also define a module that consists of more than one class. Example 10-3 is a sketch of a module that defines various classes representing geometric shapes.

Example 10-3. A module of shapes classes

 /**  * com/davidflanagan/Shapes.js: a module of classes representing shapes  *  * This module defines classes within the com.davidflanagan.shapes namespace  * This module requires the com/davidflanagan/Class.js module  **/ // First, check for the Class module var com;  // Declare global symbol before testing for its presence if (!com || !com.davidflanagan || !com.davidflanagan.Class)     throw new Error("com/davidflanagan/Class.js has not been loaded"); // Import a symbol from that module var define = com.davidflanagan.Class.define; // We know from the test for the Class module that the com.davidflanagan // namespace exists, so we don't have to create it here. // We just create our shapes namespace within it. if (com.davidflanagan.shapes)     throw new Error("com.davidflanagan.shapes namespace already exists"); // Create the namespace com.davidflanagan.shapes = {}; // Now define classes, storing their constructor functions in our namespace com.davidflanagan.shapes.Circle = define({ /* class data here */ }); com.davidflanagan.shapes.Rectangle = define({ /* class data here */ }); com.davidflanagan.shapes.Triangle = define({ /* class data here */}); 

10.1.3. Module Initialization Code

We tend to think of JavaScript modules as collections of functions (or classes). But, as is clear from the previous examples, modules do more than just define functions to be invoked later. They also run code when first loaded to set up and populate their namespace. A module can run any amount of this kind of one-shot code, and it is perfectly acceptable to write modules that define no functions or classes and simply run some code. The only rule is that they must not clutter the global namespace. The best way to structure a module of this sort is to put the code inside an anonymous function that is invoked immediately after being defined:

 (function( ) {  // Define an anonymous function.  No name means no global symbol     // Code goes here     // Any variables are safely nested within the function,     // so no global symbols are created. })( );          // End the function definition and invoke it. 

Some modules can run their own initialization code when they are loaded. Others need to have an initialization function invoked at a later time. This is common in client-side JavaScript: modules designed to operate on HTML documents usually need initialization code triggered when the HTML document has finished loading into the web browser.

A module can take a passive approach to the initialization problem by simply defining and documenting an initialization function and having the user of the module invoke the function at an appropriate time. This is a safe and conservative approach, but it requires an HTML document to have enough JavaScript code within it to at least initialize the modules that will be acting on it.

There is a school of thought (called unobtrusive JavaScript and described in Section 13.1.5.) that says that modules should be completely self-contained and that HTML documents should not contain any JavaScript code. To write modules that are unobtrusive to this degree, modules must be able to actively register their initialization functions so they are invoked at the appropriate time.

Example 10-5 at the end of this chapter includes an initialization solution that allows modules to actively register initialization functions. When run in a web browser, all registered functions are automatically invoked in response to the "onload" event sent by the web browser. (You'll learn about client-side events and event handlers in Chapter 17.)




JavaScript. The Definitive Guide
JavaScript: The Definitive Guide
ISBN: 0596101996
EAN: 2147483647
Year: 2004
Pages: 767

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net