The third episode is dealing with global variables and encapsulation. Let's start with globals. Global variables have been a curse in almost every programming language for decades, but many Javascript developers (most of them I know) still use global variables and functions all the time. Some of them may accidentally define functions which are not global (especially with jQuery), but don't really understand the difference. Why do they use globals? I think there are multiple reasons to it. First of all, it's because many web developers don't have real programming background, but instead they were once forced to widen their view of world from plain HTML to Javascript. Secondly, JavaScript has implied global variables and you may be using global variables by accident; if you do not explicitly declare a variable, a global variable is implicitly declared for you. And last but not least, Javascript has been written this way in the past; writing Javascript was considered just a mandatory plague and typically it was written by copycats without straining their brains too much.
In Javascript the global object is the global namespace that holds the top level functions and global variables. Variables which are not explicitly defined are implied global variables, and their names are also kept in the global object. Writing functions to global scope is roughly the same thing as writing Java classes to default package or C#'s global namespace. This inevitably leads to collisions and that's why many programming languages and markup languages have packages (e.g. Java) or namespaces (C#, XML, C++, ...). It's also the reason why global functions do not lead into reusable code which could be published as a library. In reality you must use global variables and functions, but don't put your own stuff there. I found a really nice depiction of global scope by Dmitry Baranovskiy. He compared JavaScript’s global scope to a public toilet: “you can’t avoid going in there, but try to limit your contact with surfaces when you do.”
As an example, let's assume for a second that jQuery and Underscore would have been implemented as a set of global functions. They share so many names of functions that you couldn't use them together. Everything would blow up instantly. Fortunately they are not reserving nothing but one or two slots from the global object: jQuery exposes 'jQuery' and '$', underscore.js just '_'. It doesn't matter how the functions and variables are named in the context of exposed global variable. If you are looking for a trouble, try using '$' as your global variable name. On the other hand, (sh)it also happens sooner or later when you use names like database, db, application, app and alike; the moment you start using another ill behaving library (in addition of yours) you are screwed.
In addition of keeping your stuff out of global scope, you should also embrace encapsulation. One particularly easy way to achieve this is to use RequireJS, which implements module pattern for you. Now it's time to show how to do something without global variables and keep your module internals hidden. First we define a module without any global vars. Here it is:
define( function( ) {
var counter = 0;
function incr( ) {
counter++;
}
return {
getAndIncrement : function( ) {
var tmp = counter;
incr( );
return tmp;
},
reset : function( ) {
counter = 0;
}
};
} );
Why aren't the functions and variables global in example above? That's because they are defined in the scope of anonymous factory function, the one and only parameter to define. They cannot be referenced from outside of their enclosing scope. The object literal returned from the factory function is going to be the public interface of your module and thus the consumer side of our module could look like this:
<script type="text/javascript">
require( [ 'counter' ],function( cnt ) {
alert( cnt.getAndIncrement( ) );
alert( cnt.getAndIncrement( ) );
cnt.reset( );
alert( cnt.getAndIncrement( ) );
} );
</script>
The series of calls show values 0, 1, 0, leaving counter to value 1. If you use the same module again (maybe another script block later in the same page), you can see that counter variable is shared between those scripts. The reason is that module loader executes the factory function (a function passed to define) just once and thus the counter variable declared inside the module is allocated and initialized just once. If this is not what we want, we can modify our module a little bit, so that it returns constructor function instead. This makes it possible to create multiple instances of our world-famous counter object. The new version now looks like this:
define( function( ) {
function incr( self ) {
self.counter++;
}
function Counter( ) {
this.counter = 0;
};
Counter.prototype.getAndIncrement = function( ) {
var tmp = this.counter;
incr( this );
return tmp;
};
Counter.prototype.reset = function( ) {
counter = 0;
};
// Return constructor function
return Counter;
} );
while the consumer could look like this:
<script type="text/javascript">
require( [ 'counter2' ],function( Counter ) {
var c1 = new Counter( );
var c2 = new Counter( );
alert( c1.getAndIncrement( ) );
alert( c2.getAndIncrement( ) );
alert( c1.getAndIncrement( ) );
alert( c2.getAndIncrement( ) );
c1.reset( );
alert( c1.getAndIncrement( ) );
} );
</script>
This time the series of alerts show values 0, 0, 1, 1, 2. Oops, what just happened? Why didn't the reset function work? Because the code is missing this-keyword in front of counter. Also, because there's no var keyword it actually sets a global variable called counter to value 0. To fix it we just add this-keyword, and now the code of function reset looks like this:
Counter.prototype.reset = function( ) {
this.counter = 0;
};
The series of alerts now show values 0, 0, 1, 1, 0. That's what we expected.
In conclusion, if you are writing nothing but random shit I would suggest you do it right. It doesn't mean huge overhead neither in performance nor productivity and then you in the right track from the beginning. Read these Global domination and JavaScript module-pattern in depth for more information.
At least one more episode is still coming, and it's about optimizations.
No comments:
Post a Comment