Javascript modular programming details

  • 2020-03-30 04:28:56
  • OfStack

Modular programming is a very common Javascript programming pattern. It generally makes the code easier to understand, but there are many good practices that are not yet well known.

basis

Let's start with a brief overview of some of the modular patterns that have emerged since Eric Miraglia (developer of YUI) first blogged about them three years ago. If you're already familiar with these modular patterns, you can skip this section and start reading "advanced patterns."

Anonymous closure

This is the basic structure that makes everything possible, and it's also the best feature of Javascript. We will simply create an anonymous function and execute it immediately. All the code will run in this function, live in a closure that provides privatization, and it will be sufficient for the variables in those closures to run through the life of our application.


(function () {
    // ... all vars and functions are in this scope only
    // still maintains access to all globals
}());

Notice that this pair of outermost parentheses wraps the anonymous function. This is necessary because of the language nature of Javascript. In js, statements starting with the keyword function are always considered function declarations. Wrapping this code in parentheses lets the interpreter know that this is a function expression.

Global variable import

Javascript has a feature called implicit global variables. Wherever a variable name is used, the interpreter reverses the scope chain to find the var declaration for that variable. If the var declaration statement is not found, the variable is treated as a global variable. If the variable is used in an assignment statement, and the variable does not exist, a global variable is created. This means that it is easy to use or create global variables in anonymous closures. Unfortunately, this can make the code extremely difficult to maintain, because it's hard to tell which variables are global at a glance.

Fortunately, our anonymous functions provide an easy workaround. Simply passing the global variable as an argument to our anonymous function gives us cleaner and faster code than the implicit global variable. Here's an example:


(function ($, YAHOO) {
    // now have access to globals jQuery (as $) and YAHOO in this code
}(jQuery, YAHOO));

Module export

Sometimes you not only want to use global variables, you also want to declare them for reuse. We can do this easily by exporting them -- by the return value of the anonymous function. Doing so will result in a basic prototype of a modular pattern, followed by a complete example:


var MODULE = (function () {
    var my = {},
        privateVariable = 1;
    function privateMethod() {
        // ...
    }
    my.moduleProperty = 1;
    my.moduleMethod = function () {
        // ...
    };
    return my;
}());

Note that we have declared a global MODULE called MODULE, which has two public properties: a method called module.modulemethod and a variable called module.moduleproperty. In addition, it maintains a private built-in state that leverages anonymous function closures. At the same time, we can easily import the required global variables and use this modular pattern as we learned earlier.

Advanced mode

The basics described in the previous section are sufficient for many situations, and now we can take this modular pattern one step further and create more powerful, extensible structures. Let's introduce each of these advanced patterns, starting with the MODULE MODULE.

Zoom mode

The entire module must be a limitation of the modular pattern in one file. Anyone working on a large project will understand the value of splitting js into multiple files. Fortunately, we have a great implementation to zoom in on the module. First, we import a module, add properties to it, and then export it. Here's an example -- zooming in on it from the original MODULE:


var MODULE = (function (my) {
    my.anotherMethod = function () {
        // added method...
    };
    return my;
}(MODULE));

We use the var keyword to ensure consistency, although it is not required here. After this code is executed, our MODULE already has a new public method called module.anothermethod. The zoom file also maintains its own private built-in state and imported objects.

Wide amplification mode

Our example above requires our initialization module to be executed first, and then the scaling module to be executed, although sometimes this may not be necessary. One of the best things a Javascript application can do to improve performance is execute scripts asynchronously. We can create flexible multi-part modules that can be loaded in any order with a wide zoom mode. Each file needs to be organized as follows:


var MODULE = (function (my) {
    // add capabilities...
    return my;
}(MODULE || {}));

In this pattern, the var expression is required. Note that this import creates a MODULE if it has not already been initialized. This means that you can use a tool like LABjs to load all your module files in parallel without being blocked.

Compact amplification mode

The wide zoom mode is great, but it also imposes some limitations on your module. Most importantly, you cannot safely override the properties of a module. You also can't use properties from other files when initializing them (but you can use them at runtime). The compact zoom mode contains a sequence of loading sequences and allows overwriting of properties. Here's a simple example (zoom in on our original MODULE) :


var MODULE = (function (my) {
    var old_moduleMethod = my.moduleMethod;
    my.moduleMethod = function () {
        // method override, has access to old through old_moduleMethod...
    };
    return my;
}(MODULE));

We overwrite the implementation of module.modulemethod in the above example, but you can maintain a reference to the original method if needed.

Cloning and inheritance


var MODULE_TWO = (function (old) {
    var my = {},
        key;
    for (key in old) {
        if (old.hasOwnProperty(key)) {
            my[key] = old[key];
        }
    }
    var super_moduleMethod = old.moduleMethod;
    my.moduleMethod = function () {
        // override method on the clone, access to super through super_moduleMethod
    };
    return my;
}(MODULE));

This is probably the least flexible option. It does make the code clean, but at the cost of flexibility. As I wrote above, if a property is an object or function, it will not be copied, but will become a second reference to that object or function. Changing one of them will change the other. . You can solve this object cloning problem with recursive cloning, but functional cloning probably won't, so eval might. Therefore, I only describe this method in this article in consideration of the completeness of the article.

Private variables across files

Dividing a module into multiple files has a major limitation: each file maintains its own private variables and has no access to the private variables of other files. But the problem can be solved. Here's an example of maintaining a wide zoom module that spans private variables across files:


var MODULE = (function (my) {
    var _private = my._private = my._private || {},
        _seal = my._seal = my._seal || function () {
            delete my._private;
            delete my._seal;
            delete my._unseal;
        },
        _unseal = my._unseal = my._unseal || function () {
            my._private = _private;
            my._seal = _seal;
            my._unseal = _unseal;
        };
    // permanent access to _private, _seal, and _unseal
    return my;
}(MODULE || {}));

All files can set properties on their respective _private variable, and it understands that they can be accessed by other files. Once the MODULE is loaded, the application can call module._seal () to prevent external calls to internal _private. If the module needs to be reenlarged, the internal method in any file can call _unseal() before loading the new file, and again after the new file has executed. I use this pattern in my work today, and I haven't seen it anywhere else. I think this is a very useful model, and it's worth writing an article about the model itself.

The child module

Our last mode of progression is obviously the simplest. There are many excellent examples of creating submodules. This is like creating a normal module:


MODULE.sub = (function () {
    var my = {};
    // ...
    return my;
}());

Although this may seem simple, I think it's worth mentioning here. Submodules have all the advanced advantages of general modules, including magnification mode and privatized state.

conclusion

Most advanced patterns can be combined to create a more useful pattern. If I were really to recommend a modular pattern for designing complex applications, I would choose to combine the wide-zoom pattern, private variables, and submodules.

I haven't thought about the performance of these patterns yet, but I'd rather turn this into a simpler way of thinking: if a modular pattern has good performance, it does a great job of minimizing it, making it faster to download this script file. Using wide zoom mode allows for simple non-blocking parallel downloads, which speeds up the download. Initialization time may be slightly slower than other methods, but it's worth the tradeoff. As long as global variable imports are accurate, runtime performance should not be affected, and it is possible to get faster runs in submodules by shortening the reference chain with private variables.

To conclude, here is an example of a submodule dynamically loading itself into its parent (creating it if the parent does not exist). For simplicity, I'm going to remove the private variables, and of course it's easy to add the private variables. This programming pattern allows an entire complex hierarchical code base to be loaded in parallel by submodules.


var UTIL = (function (parent, $) {
    var my = parent.ajax = parent.ajax || {};
    my.get = function (url, params, callback) {
        // ok, so I'm cheating a bit :)
        return $.getJSON(url, params, callback);
    };
    // etc...
    return parent;
}(UTIL || {}, jQuery));

This article summarizes current best practices for "Javascript modular programming" and shows how to put them into practice. This isn't a beginner's tutorial, but you can understand it with a little bit of basic Javascript syntax.


Related articles: