Modular programming is a very common Javascript programming pattern. It can generally make the code easier to understand, but there are many excellent practices that are not well known.
Base
Let's first briefly outline some modular patterns since Eric Miraglia (the developer of YUI) first published a blog three years ago describing modular patterns. If you are already very familiar with these modular modes, you can skip this section directly and start reading from "Advanced Mode".
Anonymous closure
This is a basic structure that makes everything possible, and it is also the best feature of Javascript. We will simply create an anonymous function and execute it immediately. All code will run within this function and live in a closure that provides privatization, which is enough to enable the variables in these closures to run through the entire life of our application.
The code copy is as follows:
(function () {
// ... all vars and functions are in this scope only
// still maintains access to all globals
}());
Note the outermost brackets of this pair wrapping anonymous functions. Because of the language characteristics of Javascript, this is necessary for brackets. Statements starting with the keyword function in js are always considered function declarative. Wrap this code in brackets to let the interpreter know that this is a function expression.
Global variable import
Javascript has a feature called implicit global variables. No matter where a variable name is used, the interpreter will find the var declaration statement of the variable in reverse according to the scope chain. If the var declaration statement is not found, then this variable will be considered a global variable. If this variable is used in an assignment statement and the variable does not exist, a global variable will be created. This means it is easy to use or create global variables in anonymous closures. Unfortunately, this makes the code written extremely difficult to maintain, because for people's intuitive feelings, it is impossible to tell at a glance that those variables are global.
Fortunately, our anonymous functions provide simple workarounds. Just pass the global variable as a parameter into our anonymous function, you can get clearer and faster code than the implicit global variable. Here is an example:
The code copy is as follows:
(function ($, YAHOO) {
// now have access to globals jQuery (as $) and YAHOO in this code
}(jQuery, YAHOO));
Module export
Sometimes you want not only to use global variables, you also want to declare them for repeated use. We can do this easily by deriveting them - through the return value of anonymous functions. Doing so will complete a basic modular model, and the following will be a complete example:
The code copy is as follows:
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 2 public properties: a method called MODULE.moduleMethod and a variable called MODULE.moduleProperty. In addition, it maintains a private built-in state that utilizes anonymous function closures. At the same time, we can easily import the required global variables and use this modular pattern as we have learned before.
Advanced mode
The basis described in the above section is enough to deal with many situations, and now we can further develop this modular model to create more powerful and scalable structures. Let's start with the MODULE module and introduce these advanced modes one by one.
Zoom mode
The entire module must be a limitation of modular mode in one file. Anyone who is involved in a large project will understand the value of splitting multiple files with js. Fortunately, we have a great implementation to amplify the modules. First, we import a module, add properties to it, and finally export it. Here is an example - zoom in from the original MODULE:
The code copy is as follows:
var MODULE = (function (my) {
my.anotherMethod = function () {
// added method...
};
return my;
}(MODULE));
We use the var keyword to ensure consistency, although it is not necessary here. After this code is executed, our module already has a new public method called MODULE.anotherMethod. This magnified file will also maintain its own private built-in state and imported objects.
Wide zoom mode
Our example above requires our initialization module to be executed first and then amplified module to be executed, and of course sometimes this may not necessarily be necessary. One of the best things Javascript applications can do to improve performance is to execute scripts asynchronously. We can create flexible multipart modules and enable them to be loaded in any order through a wide zoom mode. Each file needs to be organized in the following structure:
The code copy is as follows:
var MODULE = (function (my) {
// add capabilities...
return my;
}(MODULE || {}));
In this pattern, the var expression makes it necessary. Note that if MODULE has not been initialized, this import statement will create a MODULE. This means you can use a tool like LABjs to load all your module files in parallel without being blocked.
Tight enlargement mode
The wide-enlarge mode is very good, but it will also put some limitations on your module. Most importantly, you cannot safely overwrite the properties of a module. You can't use properties from other files when initializing (but you can use them when running). Tight enlargement mode contains a loaded sequence and allows overriding properties. Here is a simple example (zoom in our original module):
The code copy is as follows:
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 override the implementation of MODULE.moduleMethod in the example above, but when needed, we can maintain a reference to the original method.
Clone and inheritance
The code copy is as follows:
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 model is probably the most inflexible option. It does make the code look neat, but that comes 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 the second reference to the object or function. If you modify one of them, you will modify the other at the same time (Translator's note: because they are simply one!). This can be solved by recursive cloning process, but function cloning may not be solved, maybe it can be solved with eval. Therefore, I am telling this method in this article only takes into account the integrity of the article.
Cross-file private variables
There is a significant limitation to splitting a module into multiple files: each file maintains its own private variables and cannot access private variables for other files. But this problem can be solved. Here is an example of a wide-enlarge module that maintains private variables across files:
The code copy is as follows:
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 variables, and it understands that they can be accessed by other files. Once this module is loaded, the application can call MODULE._seal() to prevent external calls to internal _private. If this module needs to be re-magnified, the internal method in any file can call _unseal() before loading the new file, and call _seal() again after the new file is executed. I use this pattern at work now and I haven't seen it anywhere else. I think this is a very useful model and it is worth writing an article about this model itself.
Submodules
Our last advanced mode is obviously the easiest. There are many excellent examples of creating submodules. It's like creating a normal module:
The code copy is as follows:
MODULE.sub = (function () {
var my = {};
// ...
return my;
}());
Although this seems simple, I think it is worth mentioning here. Submodules have all the advanced advantages of general modules, including amplification mode and privatization state.
in conclusion
Most advanced modes can be combined to create a more useful mode. If I really want to recommend a modular pattern for designing complex applications, I would choose to combine wide amplification mode, private variables, and submodules.
I haven't considered the performance of these modes, but I'd rather turn this into a simpler way of thinking: if a modular mode has good performance, it can do a great job of minimizing it, making downloading this script file faster. Using the wide enlargement mode allows simple non-blocking parallel downloads, which speeds up downloads. The initialization time may be slightly slower than other methods, but it is worth it after weighing the pros and cons. As long as the global variable import is accurate, runtime performance should be affected, and it is also possible to achieve faster running speeds in submodules by shortening the reference chain with private variables.
As a conclusion, here is an example of a child module dynamically loading itself into its parent module (creating it if the parent module does not exist). For simplicity, I removed the private variables, and of course it is very simple to add private variables. This programming pattern allows a whole complex hierarchical structure code base to be loaded in parallel through submodules.
The code copy is as follows:
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 the current best practices of "Javascript modular programming" and explains how to put it into practice. Although this is not a primary tutorial, you can understand it by just a little understanding of the basic syntax of Javascript.