A Basic Javascript Module Pattern
Big JavaScript files, who likes them? Not me. For a while I had a real problem with them (as many do) but I have found a way to avoid the problem (not that these ideas are all original). And I am very happy to have this problem solved. We are using these ideas at work and I figured I would share what we have been doing that has worked really well. Later I'll share some things that haven't, which some of you may find even more beneficial.
We should start this discussion with a simple sample of some JavaScript and point out some problems. Here we go.
console.log('--------------- snippet 1 ---------------');
var count = 0;
function increment() {
count++;
randomThing = 'wut?';
}
function decrement() {
count--;
var iAmHidden = 10;
}
console.log('count', count);
increment();
console.log('count incremented', count);
decrement();
console.log('count decremented', count);
increment();
console.log('count is global?!?!', window.count);
console.log('randomThing is global?!?!', window.randomThing);
console.log('wait, the functions are global too?!?!',
window.increment, window.decrement);
console.log('cannot see "iAmHidden", so this will be undefined',
window.iAmHidden);
As far as JavaScript goes, that is some pretty easy stuff. Unfortunately, this code can be very problematic because of the globals it is creating. I won't go into the reasoning here but one of the basic axioms of programming (so you probably know this) is that globally held state is generally a bad idea, and this script offends that principle all over the place. All functions (such as 'increment' and 'decrement') and variables (such as 'count' and 'randomThing') above are held in global state (save one), as you can see from the fact that they are on the window object. The exception is the variable 'iAmHidden', which is scoped within the function it is declared because of the var in front of it. We can remove almost all of the global scope by wrapping this stuff in a self-executing function.
console.log('--------------- snippet 2 ---------------');
(function () {
var count = 0;
function increment() {
count++;
randomThing = 'wut?';
}
function decrement() {
count--;
var iAmHidden = 10;
}
console.log('count', count);
increment();
console.log('count incremented', count);
decrement();
console.log('count decremented', count);
increment();
})();
console.log('count is no longer global!', window.count);
console.log('randomThing is global?!?!', window.randomThing);
console.log('the functions are no longer global!',
window.increment, window.decrement);
console.log('still cannot see "iAmHidden"', window.iAmHidden);
This is a lot better, but we still created a global variable. Without the var keyword, that 'randomThing' variable will essentially float up the context till it gets to something that matches or the top, which is the window object, and it will scope itself there. So we have created a global variable. The easiest way to fix this is to just put a var in front of it and problem solved. Along with that, we can push this function into strict mode so we can't accidentally do that again.
console.log('--------------- snippet 3 ---------------');
(function () {
"use strict";
var count = 0;
function increment() {
count++;
var randomThing = 'wut?';
}
function decrement() {
count--;
var iAmHidden = 10;
}
console.log('count', count);
increment();
console.log('count incremented', count);
decrement();
console.log('count decremented', count);
increment();
})();
console.log('count is no longer global!', window.count);
console.log('randomThing is no longer global!', window.randomThing);
console.log('the functions are no longer global!',
window.increment, window.decrement);
console.log('still cannot see "iAmHidden"', window.iAmHidden);
So what's going on here? In this case this self-executing function that wraps this whole thing [the "(function() { ... })()" bit] creates a scope for everything. If you run this, you see that the functions do indeed get executed but that there is now no global scope at all. Yay! This is also the beginning of a basic module pattern. Let's expand on things a bit.
Namespacing
As is, limiting our scope has limited our ability to reference that code. There is no way for something outside of that self-executing function to increment or decrement, so we have cut off the functionality from the app, which isn't all that helpful. We can solve this without poluting the global scope a ton by employing namespacing, a practice familiar to most everyone who uses any modern programming language. In JavaScript you do namespacing by creating objects and assigning other bits to those objects. Example time. Let's say your company's name is RabidMonkey. You could rewrite all this and get some code that's usable outside of its function like so:
console.log('--------------- snippet 4 ---------------');
RabidMonkey = {};
(function () {
"use strict";
var count = 0;
RabidMonkey.increment = function() {
count++;
var randomThing = 'wut?';
}
RabidMonkey.decrement = function() {
count--;
var iAmHidden = 10;
}
RabidMonkey.getCount = function() {
return count;
}
})();
console.log('count is still not global!', window.count);
console.log('but you can get to it via the RabidMonkey "namespace"',
RabidMonkey.getCount());
RabidMonkey.increment();
console.log('count incremented', RabidMonkey.getCount());
RabidMonkey.decrement();
console.log('count decremented', RabidMonkey.getCount());
console.log('the functions are no longer global and'
+ ' have to be referenced through the RabidMonkey "namespace"',
window.increment, window.decrement);
So controlling global state while still being able to use things is quite easily doable in JavaScript. You just have to follow this pattern or a similar one (there are some interesting variations). Essentially what we have done here is create one global variable called 'RabidMonkey' and attached all functionality to it. The global scope of the browser is essentially preserved. Note that the count variable is not referencable outside of the self-executing function. The only things that can be referenced are the functions explitly tied to the namespace.
But I would actually make one more change. The assumption would be that with the above pattern, 'RabidMonkey' now becomes the new global for the code that you write for RabinMonkey. If all variables and functions live on it you may have avoided scope conflict with third-party libraries but you are setting yourself up with conflicts in your own code. So we can break this down further and create an object for these bits of functionality, similar to how we created the namespace.
console.log('--------------- snippet 5 ---------------');
RabidMonkey = {};
(function () {
"use strict";
var count = 0;
var obj = {
increment: function () {
count++;
var randomThing = 'wut?';
},
decrement: function () {
count--;
var iAmHidden = 10;
},
getCount: function () {
return count;
}
};
RabidMonkey.Counter = obj;
})();
RabidMonkey.Counter.increment();
console.log('count incremented', RabidMonkey.Counter.getCount());
RabidMonkey.Counter.decrement();
console.log('count decremented', RabidMonkey.Counter.getCount());
So those are the basic concepts you need to control your global scope by using a basic module pattern. You may not realize it but this is also a really big step towards modular JavaScript. We will dig more into that next time with a application that is a little less sample-ish.