I'm not claiming to originate these ideas; they have been in use, in some variations, for several years on pages throughout the web.
Much as with Tapestry's Java code, it is high time that there is a distinction between public JavaScript functions and private, internal functions. I've come to embrace modular JavaScript namespacing.
One of the challenges of JavaScript is namespacing: unless you go to some measures, every var
and function
you define gets attached to the global window
object. This can lead to name collisions ... hilarity ensues.
How do you avoid naming collisions? In Java you use packages ... but JavaScript doesn't have those. Instead, we define JavaScript objects to contain the variables and functions. Here's an example from Tapestry's built-in library:
Tapestry = { FORM_VALIDATE_EVENT : "tapestry:formvalidate", onDOMLoaded : function(callback) { document.observe("dom:loaded", callback); }, ajaxRequest : function(url, options) { ... }, ... };
Obviously, just an edited excerpt ... but even here you can see the clumsy prototype for an abstraction layer. The limitation with this technique is two fold:
- Everything is public and visible. There's no private modifier, no way to hide things.
- You can't rely on using
this
to reference other properties in the same object, at least not inside event handler methods (wherethis
is often the window object, rather than what you'd expect).
These problems can be addressed using a key feature of JavaScript: functions can have embedded variable and functions that are only visible inside that function. We can start to recode Tapestry as follows:
Tapestry = { FORM_VALIDATE_EVENT : "tapestry:formvalidate" }; function initializeTapestry() { var aPrivateVariable = 0; function aPrivateFunction() { } Tapestry.onDOMLoaded = function(callback) { document.observe("dom:loaded", callback); }; Tapestry.ajaxRequest = function(url, options) { ... }; } initializeTapestry();
Due to the rules of JavaScript closures, aPrivateVariable
and aPrivateFunction()
can be referenced from the other functions with no need for the this
prefix; they are simply values that are in scope. And they are only in scope to functions defined inside the initializeTapestry()
function.
Further, there's no longer the normal wierdness with the this
keyword. In this style of coding, this
is no longer relevant, or used. Event handling functions have access to variables and other functions via scoping rules, not through the this
variable, so it no longer matters that this
is often not what you'd expect ... and none of the nonsense about binding this
back to the expected object that you see in Prototype and elsewhere. Again, this is a more purely functional style of JavaScript programming.
Often you'll see the function definition and evaluation rolled together:
Tapestry = { FORM_VALIDATE_EVENT : "tapestry:formvalidate" }; (function() { var aPrivateVariable = 0; function aPrivateFunction() { } Tapestry.onDOMLoaded = function(callback) { document.observe("dom:loaded", callback); }; Tapestry.ajaxRequest = function(url, options) { ... }; })();
That's more succinct, but not necessarily more readable. I've been prototyping a modest improvement in TapX, that will likely be migrated over to Tapestry 5.3.
Tapx = { extend : function(destination, source) { if (Object.isFunction(source)) source = source(); Object.extend(destination, source); }, extendInitializer : function(source) { this.extend(Tapestry.Initializer, source); } }
This function, Tapx.extend()
is used to modify an existing namespace object. It is passed a function that returns an object; the function is invoked and the properties of the returned object are copied onto the destintation
namespace object (the implementation of extend()
is currently based on utilities from Prototype, but that will change). Very commonly, it is Tapestry.Initializer
that needs to be extended, to support initialization for a Tapestry component.
Tapx.extendInitializer(function() { function doAnimate(element) { ... } function animateRevealChildren(element) { $(element).addClassName("tx-tree-expanded"); doAnimate(element); } function animateHideChildren(element) { $(element).removeClassName("tx-tree-expanded"); doAnimate(element); } function initializer(spec) { ... } return { tapxTreeNode : initializer }; });
This time, the function defines internal functions doAnimate()
, animateRevealChildren()
, animateHideChildren()
and initializer()
. It bundles up initializer()
at the end, exposing it to the rest of the world as Tapestry.Initializer.tapxTreeNode
.
This is the pattern going forward as Tapestry's tapestry.js library is rewritten ... but the basic technique is applicable to any JavaScript application where lots of seperate JavaScript files need to be combined together.
7 comments:
Hi Howard,
I repeat myself but try Dojo.
Cheers,
Numa
Nice Post.
in jQuery we use a lot of (function($) { /* some code that uses $ */ })(jQuery) in order to be compatible with prototype in case of use outside the clojure.
We also use lot of (function($){
/** Container of functions that may be invoked by the Tapestry.init() function. */
$.extend(Tapestry.Initializer, {
jqGrid: function(specs) {
...
Hmmm.. I still prefer the "Header" way of doing things.. :
var MyClass = (function() {
function privateF() {}
function publicF(){]
return {
'publicF': publicF
};
});
This way you 'declare' what you want to share.
You can use a constructor like
var Tapestry = ( function () {
var that = {};
function private_method() {}
var private_membr = 2;
that.method1 = function () {};
that.smthng = 3;
//...
return that;
}() )
like Douglas Crockford does
There's a lot of variations; what I wanted to do was avoid the
(function() { ... return { ... }; })();
... which I don't find readable.
You guys are writing a lot of code. This is what I use. What's wrong with it?
if (!window['myNamespace'])
window['myNamespace'] = {};
myNamespace.myFunction = function(x) {
alert(x);
}
myNamespace.myFunction('blah');
@Stimpy77,
What you have looks like some of Tapestrys current JS code base. It does not allow for private variables or private functions.
Post a Comment