0

Introduction of Maintainable Javascript: Avoid Globals

The JavaScript execution environment is unique in a lot of ways. One of those ways is in the use of global variables and functions. The default JavaScript execution environment is, in fact, defined by the various globals available to you at the start of script execution. All of these are said to be defined on the global object, a mysterious object that represents the outermost context for a script.

In browsers, the window object is typically overloaded to also be the global object, so any variable or function declared in the global scope becomes a property of the window object. For example:

var color = “red"

function `sayColor()` {
   alert(color);
}

console.log(window.color);               // "red"
console.log(typeof window.sayColor);     // “function"

In this code, the global variable color and the global function sayColor() are defined. Both are added to the window object as properties even though they weren’t explicitly set to do so.

1. The Problems with Globals

Creating globals is considered a bad practice in general and is specifically problematic in a team development environment. Globals create a number of nontrivial maintenance issues for code going forward. The more globals, the greater the possibility that errors will be introduced due to the increasing likelihood of a few common problems.

1.1. Naming Collisions

The potential for naming collisions increases as the number of global variables and functions increase in a script, as do the chances that you’ll use an already declared variable accidentally. The easiest code to maintain is code in which all of its variables are defined locally.

For instance, consider the sayColor() function from the previous example. This function relies on the global variable color to function correctly. If sayColor() were defined in a separate file than color, it would be hard to track down:

function `sayColor()` {
  alert(color);             // Bad: where'd this come from?
}

Further, if color ends up being defined in more than one place, the result of sayColor() could be different depending on how this function is included with the other code.

The global environment is where native JavaScript objects are defined, and by adding your own names into that scope, you run the risk of picking a name that might be provided natively by the browser later on. The name color, for example, is definitely not a safe global variable name. It’s a plain noun without any qualifiers, so the chances of collision with an upcoming native API, or another developer’s work, is quite high.

1.2. Code Fragility

A function that depends on globals is tightly coupled to the environment. If the environment changes, the function is likely to break. In the previous example, the sayColor() method will throw an error if the global color variable no longer exists. That means any change to the global environment is capable of causing errors throughout the code. Also, globals can be changed at any point by any function, making the reliability of their values highly suspect. The function from the previous example is much more maintainable if color is passed in as an argument:

function sayColor(color) {
  alert(color);
}

This version of the function has no global dependencies and thus won’t be affected by changes to the global environment. Because color is now a named argument, all that matters is that a valid value is passed into the function. Other changes will not affect this function’s ability to complete its task. When defining functions, it’s best to have as much data as possible local to the function. Anything that can be defined within the function should be written as such; any data that comes from outside the function should be passed in as an argument. Doing so isolates the function from the surrounding environment and allows you to make changes to either without affecting the other.

1.3. Difficulty Testing

Any function that relies on global variables to work requires you to recreate the entire global environment to properly test that function. Effectively, this means that you’re not just managing changes in one global environment, you’re managing them in two global environments: production and testing. Add to that the cost of keeping those in sync, and you’ve got the beginnings of a maintenance nightmare that isn’t easily untangled.

2. Accidental Globals

One of the more insidious parts of JavaScript is its capacity for creating globals accidentally. When you assign a value to variable that has not previously been defined in a var statement, JavaScript automatically creates a global variable. For example:

function doSomething() {
  var count = 10;
  title = "Maintainable JavaScript";           // Bad: global
}

This code represents a very common coding error that accidentally introduces a global variable. The author probably wanted to declare two variables using a single var statement but accidentally included a semicolon after the first variable instead of a comma. The result is the creation of title as a global variable.

The problem is compounded when you’re trying to create a local variable with the same name as a global variable. For example, a global variable named count would be shadowed by the local variable count in the previous example. The function then has access only to the local count unless explicitly accessing the global using window.count. This arrangement is actually okay.

Omitting the var statement accidentally might mean that you’re changing the value of an existing global variable without knowing it. Consider the following:

function doSomething() {
  var count = 10;
      name = "Nicholas";            // Bad: global
}

In this example, the error is even more egregious because name is actually a property of window by default. When the window.name property is most frequently used with frames or iframes, and is how links are targeted to show up in certain frames or tabs when clicked, changing name inadvertently could affect how links navigate in the site.

A good rule of thumb is to always use var to define variables, even if they are global. This way is a lot less error prone than omitting the var in certain situations.

2.1. Avoiding Accidental Globals

JavaScript won’t warn when you’ve accidentally created a global variable by default. That’s when tools like JSLint and JSHint come into play. Both tools will warn you if you assign a value to a previously undeclared variable. For example:

foo = 10;

Both JSLint and JSHint will issue the warning “‘foo’ is not defined” to let you know that the variable foo was never declared using a var statement. JSLint and JSHint are also smart enough to notice that you’re accidentally changing the value of certain globals. In the example that overwrites the value of name, JSLint and JSHint warn that the variable in question is read-only. In fact, the variable isn’t readonly; however, it should be treated as such, because changing a native global almost always leads to errors. The same warning is issued if you try to assign other globals such as window and document.

Strict mode, which changes some of the rules for parsing and executing JavaScript, offers a solution to this problem. By adding "use strict" to the top of a function, you instruct the JavaScript engine to apply more rigorous error handling and syntax checking before executing the code. One of these changes is the ability to detect assignment to undeclared variables. When this happens, the JavaScript engine throws an error. For example:

"use strict";
foo = 10;       // ReferenceError: foo is not defined

If you try to run this code in an environment supporting strict mode (Internet Explorer 10+, Firefox 4+, Safari 5.1+, Opera 12+, or Chrome), the second will throw a ReferenceError with a message of “foo is not defined.”

3. The One-Global Approach

The one-global approach is used by all popular JavaScript libraries:

• YUI defines a single YUI global object.

• jQuery actually defines two globals, $ and jQuery. The latter was added only for compatibility when used on a page with other libraries also using $.

• Dojo defines a single dojo global object.

• The Closure library defines a single goog global object.

The idea behind the one-global approach is to create a single global with a unique name (one that is unlikely to be used by native APIs) and then attach all of your functionality onto that one global. So instead of creating multiple globals, each would-be global simply becomes a property of your one global. For instance, suppose I wanted to have an object representing each chapter in this book. The code might look like this:

function Book(title) {
  this.title = title;
  this.page = 1;
}

Book.prototype.turnPage = function(direction) {
  this.page += direction;
};

var Chapter1 = new Book("Introduction to Style Guidelines");
var Chapter2 = new Book("Basic Formatting");
var Chapter3 = new Book(“Comments");

This code creates four globals, Book, Chapter1, Chapter2, and Chapter3. The one-global approach would be to create a single global and attach each of these:

var MaintainableJS = {};

MaintainableJS.Book = function(title) {
  this.title = title;
  this.page = 1;
};

MaintainableJS.Book.prototype.turnPage = function(direction) {
  this.page += direction;
};

MaintainableJS.Chapter1 = new MaintainableJS.Book("Introduction to Style Guidelines");
MaintainableJS.Chapter2 = new MaintainableJS.Book("Basic Formatting");
MaintainableJS.Chapter3 = new MaintainableJS.Book("Comments");

This code has a single global, MaintainableJS, to which all of the other information is attached. As long as everyone on the team is aware of the single global, it’s easy to continue adding properties to it and avoid global pollution.

3.1. Namespaces

Grouping functionality into namespaces brings some order to your one global object and allows team members to understand where new functionality belongs as well as where to look for existing functionality. That design allowed teams to use one another’s code without fear of naming collisions.

You can easily create your own namespaces in JavaScript with objects, as in:

var ZakasBooks = {};

// namespace for this book
ZakasBooks.MaintainableJavaScript = {};

// namespace for another book
ZakasBooks.HighPerformanceJavaScript = {}

A common convention is for each file to declare its own namespace by creating a new object on the global. In such circumstances, the previous example pattern works fine.

There are also times when each file is simply adding to a namespace; in that case, you may want a little more assurance that the namespace already exists. That’s when a global that handles namespaces nondestructively is useful. The basic pattern to accomplish this is:

var YourGlobal = {
  namespace: function(ns) {
    var parts = ns.split("."),
          object = this,
          i, len;
    for (i=0, len=parts.length; i < len; i++) {
      if (!object[parts[i]]) {
        object[parts[i]] = {};
      }
      object = object[parts[i]];
    }

    return object;
  }
};

The variable YourGlobal can actually have any name. The important part is the namespace() method, which nondestructively creates namespaces based on the string that is passed in and returns a reference to the namespace object. Basic usage:

/*
 * Creates both YourGlobal.Books and YourGlobal.Books.MaintainableJavaScript.
 * Neither exists before hand, so each is created from scratch.
 */
YourGlobal.namespace(“Books.MaintainableJavaScript");

// you can now start using the namespace
YourGlobal.Books.MaintainableJavaScript.author = "Nicholas C. Zakas”;

/*
 * Leaves YourGlobal.Books alone and adds HighPerformanceJavaScript to it.
 * This leaves YourGlobal.Books.MaintainableJavaScript intact.
 */
YourGlobal.namespace(“Books.HighPerformanceJavaScript");

// still a valid reference
console.log(YourGlobal.Books.MaintainableJavaScript.author);

// You can also start adding new properties right off the method call
YourGlobal.namespace("Books").ANewBook = {};

Using a namespace() method on your one global allows developers the freedom to assume that the namespace exists. That way, each file can call namespace() first to declare the namespace the developers are using, knowing that they won’t destroy the namespace if it already exists. This approach also frees developers from the tedious task of checking to see whether the namespace exists before using it.

3.2. Modules

Another way developers augment the one-global approach is by using modules. A module is a generic piece of functionality that creates no new globals or namespaces. Instead, all of the code takes place within a single function that is responsible for executing a task or publishing an interface. The module may optionally have a name and a list of module dependencies.

Modules aren’t formally part of JavaScript. There is no module syntax (at least, not until ECMAScript 6), but there are some common patterns for creating modules. The two most prevalent types are YUI modules and Asynchronous Module Definition (AMD) modules.

3.2.1. YUI modules

YUI modules are, as you might expect, how you create new modules to work with the YUI JavaScript library. The concept of modules was formalized in YUI 3 and takes the following form:

YUI.add("module-name", function(Y) {

  // module body
}, "version", { requires: [ "dependency1", "dependency2" ] });

YUI modules are added by calling YUI.add() with the module name, the function to execute (called a factory function), and an optional list of dependencies. The module body is where you place all code for this module. The Y argument is an instance of YUI that has all of the required dependencies available. The YUI convention is to add module functionality as namespaces inside of each module, such as:

YUI.add("my-books", function(Y) {

  // Add a namespace
  Y.namespace(“Books.MaintainableJavaScript");

  Y.Books.MaintainableJavaScript.author = "Nicholas C. Zakas”;

}, "1.0.0", { requires: [ "dependency1", "dependency2" ] });

Likewise, the dependencies are represented as namespaces on the Y object that is passed in. So YUI actually combines the concepts of namespaces with modules to give you flexibility in the overall approach.

Use your module via the YUI().use() method and pass in one or more module names to load:

YUI().use("my-books", "another-module", function(Y) {

  console.log(Y.Books.MaintainableJavaScript.author);

});

This code starts by loading the modules named "my-books" and "another-module", ensuring that the dependencies for each are fully loaded. Then the module body is executed in the order in which the modules are specified. Last, the callback function passed to YUI().use() is executed. The Y object that is passed in has all of the changes made to it by the loaded modules, so your application code is ready to execute.

3.2.2. Asynchronous Module Definition (AMD) Modules

AMD modules have a lot in common with YUI modules. You specify a module name, dependencies, and a factory function to execute once the dependencies are loaded. These are all passed to a global define() function with the name first, then the dependencies, and then the factory function. A major difference between AMD modules and YUI modules is that each dependency is passed as a separate argument to the factory function. For example:

define("module-name", [ "dependency1", "dependency2" ],
       function(dependency1, dependency2) {

  // module body

});

So each named dependency ends up creating an object and passing it to the factory function. In this way, AMD seeks to avoid naming collisions that might occur with namespaces across modules. Instead of creating a new namespace as in a YUI module, AMD modules are expected to return their public interface from the factory function, such as:

define("my-books", [ "dependency1", "dependency2" ],
       function(dependency1, dependency2) {

  var Books = {};
  Books.MaintainableJavaScript = {
    author: "Nicholas C. Zakas"
  };

  return Books;
});

AMD modules can also be anonymous and completely omit the module name. The assumption is that the module loader can infer the module name through the JavaScript filename. So if you have a file named my-books.js and your module will only ever be loaded through a module loader, you can define your module as follows:

define([ "dependency1", "dependency2" ],
        function(dependency1, dependency2) {

  var Books = {};
  Books.MaintainableJavaScript = {
    author: "Nicholas C. Zakas"
  };

  return Books;
});

To make use of AMD modules, you need to use a compatible module loader. Dojo’s standard module loader supports loading of AMD modules, so you can load a the module "my-books" like this:

// load AMD module in Dojo
var books = dojo.require(“my-books");

console.log(books.MaintainableJavaScript.author);

Dojo also exposes itself as an AMD module named "dojo", so it can be loaded into other AMD modules.

Another module loader is RequireJS. RequireJS adds another global function called require(), which is responsible for loading the specified dependencies and then executing a callback function. For example:

// load AMD module with RequireJS
require([ "my-book" ], function(books) {

  console.log(books.MaintainableJavaScript.author);

});

The dependencies start to download immediately upon calling require(), and the callback executes as soon as all of those dependencies have loaded (similar to YUI().use()).

The RequireJS module loader has a lot of logic built in to make loading modules easy. These include mapping of names to directories as well as internationalization options. Both jQuery and Dojo are capable of using RequireJS to load AMD modules.

4. The Zero-Global Approach

It is possible to inject your JavaScript into a page without creating a single global variable. This approach is quite limited, so it is useful only in some very specific situations. The most common situation is with a completely standalone script that doesn’t have to be accessed by any other scripts. This situation may occur because all of the necessary scripts are combined into one file, or because the script is small and being inserted into a page that it shouldn’t interfere with. The most common use case is in creating a bookmarklet.

Bookmarklets are unique in that they don’t know what’s going to be on the page and don’t want the page to know that they are present. The end result is a need for a zero global embedding of the script, which is accomplished by using an immediate function invocation and placing all of the script inside of the function. For example:

(function(win) {

  var doc = win.document;

  // declare other variables here

  // other code goes here

}(window));

This immediately invoked function passes in the window object so that the scripts needn’t directly access any global variables. Inside the function, the doc variable holds a reference to the document object. As long as the function doesn’t modify window or doc directly and all variables are declared using the var keyword, this script will result in no globals being injected into the page. You can further avoid creating globals by putting the function into strict mode (for browsers that support it):

(function(win) {

  "use strict”;

  var doc = win.document;

  // declare other variables here

  // other code goes here

}(window));

This function wrapper can now be used for scripts that don’t want to create any global objects. As mentioned previously, this pattern is of limited use. Any script that needs to be used by other scripts on the page cannot use the zero-global approach. A script that must be extended or modified during runtime cannot use the zero-global approach, either. However, if you have a small script that doesn’t need to interact with any other scripts on the page, the zero-global approach is something to keep in mind.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí