Monday, December 03, 2007

Defining $ for Dojo and setting a function prototype

This is a little story about defining a $ function for Dojo, and a more fundamental discussion about setting a prototype for a function.

Defining a $ function for Dojo

I generally prefer using full namespace names when using JavaScript libraries. It helps avoid confusion when passing around code or copying/pasting code that may be used in mixed-library environments. However, if you have contained environment, you may prefer extreme brevity over namespace robustness.

So, how can we define a function named $ that works with the base Dojo functionality (the stuff you get in dojo.js: DOM querying, event handling, XHR, style, basic effects, JSON, array, language and object hierarchy helpers). Ideally, $ would be mapped to dojo.query, to allow easy node selection, but then any other dojo method, like dojo.connect() would be available via $.connect().

Here is something that works:

$ = function(){
return dojo.query.apply(dojo, arguments);
};
dojo.mixin($, dojo);


That does the basics: defines a function $ that returns the result of a dojo.query() call, then copies all the properties of dojo to the $ function object (via dojo.mixin()).

This allows for making these sorts of calls:

//Set on onclick listener for all divs in the body
$("div").onclick(function(){
alert("div clicked.");
});

//Do an XMLHttpRequest POST using data from
//a form with a DOM ID of "myFormId"
$.xhrPost({
form: $("#myFormId")[0]
});


Setting a prototype for a function

So that is nice if all I want is dojo base. However, Dojo has a neat module loader, and ideally I want to be able to do something like:

dojo.require("dojo.io.script");


Then later in the code just do a $.io.script.get(). However, this will not work if you define the $ function before doing the dojo.require() call. dojo.mixin() only copies over object properties that exist at the time of the dojo.mixin() call.

So, ideally, I want to set the prototype lookup for $ to be dojo. That way, as new things are added to the dojo object, they will be automatically available via $.

At this point I hit a wall, at least with my current understanding of JavaScript. I was hoping I could do something like this:

var Dollar = function(){
return function(){
return dojo.query.apply(dojo, arguments);
};
};
Dollar.prototype = dojo;

$ = new Dollar();


In JavaScript constructor functions, you do not have to return a value. If you do not, whatever the "this" value was inside the constructor function gets returned as the result of the call the "new" call.

However, if you return a value from the constructor, that is what is returned as the result of the "new" call. This is what Dollar does above.

What I was hoping would happen is that the returned function (that calls dojo.query()) would have its __proto__ property set to Dojo, but I think the __proto__ binding happens before the constructor function is called, so that you can access methods on "this" inside the constructor.

Suppose there was a constructor function called ConstructorFunction, and you called "new ConstructorFunction()". Here is some very crude psuedo-code for what I think the "new" call is doing:

var temp = {};
temp.__proto__ = ConstructorFunction.prototype;
return ConstructorFunction.apply(temp, arguments);


If this is what is going on, it makes sense that by returning a value from the constructor function call, I lose out on the __proto__ binding.

So why not just set the $.__proto__ value directly after $ is defined? Accessing __proto__ is strongly discouraged. It is an implementation's private implementation detail, subject to change. Also, doing that only works right now for Firefox 2 and Safari 3 (failed for IE 6 and Opera 9.24). So stop thinking about it.

I also tried something like this:

Function.prototype = dojo;
$ = new Function("return dojo.query.apply(dojo, arguments);");


but that fails for what I believe to be similar reasons as my first prototype attempt.

I am very interested if someone can suggest how I can modify the $ function's prototype lookup, so that it uses the dojo object. It is neat to think about defining an object that also supports the () operator on it (with a custom prototype).

I want something that works with today's browsers, not ECMAScript 4. Although seeing an ECMAScript 4 implementation might be instructional for the future.

9 comments:

Unknown said...

So, really simply: We just add the ability to have something like djConfig.dojo that lets you define what "dojo" is created as. By default, it's {}. So you make it use a function instead, and have a pointer from $ to dojo.

James said...

pottedmeat: That would solve the immediate example for Dojo, but I am interested in a general technique, something that could be added after the fact, without requiring internal Dojo code changes.

It seemed like an interesting thought exercise on functions and prototypes. I do not see a generic way of getting the result I want, but maybe someone else has an idea.

Anonymous said...

Instead of:
Function.prototype = dojo;
$ = new Function("return dojo.query.apply(dojo, arguments);");

try:

$ = new Function("return dojo.query.apply(dojo, arguments);");
Function.prototype = dojo.prototype;

Then anything added to the dojo's prototype will automatically be added to $'s prototype

Anonymous said...

Sorry, I meant:
$ = new Function("return dojo.query.apply(dojo, arguments);");
$.prototype = dojo.prototype;

Shane

James said...

shaneosullivan: I see two issues with that approach:

1) The dojo module loader attaches properties directly to the dojo object, not on dojo.prototype.

2) I also do not want to modify the Function.prototype since that will affect all Function derivatives. I did that in my example code above, just to see if it would work, but I would not want that change for the "final" solution.

James said...

shaneosullivan: oops, I did not address your second post: setting $.prototype will not work in that case, since $ is already created.

Setting $.prototype would only help things that use $ in their prototype chain (as I understand it).

And for what it is worth, I did try an example like that, but it did not work.

What I really want to do in that line is something like $.__proto__ = dojo, but I consider that verboten, and does not work cross browser.

Unknown said...

$ = dojo = dojo.mixin(function(){
return dojo.query.apply(dojo, arguments);
}, dojo);

// And test
$.addOnLoad(function(){
console.debug($("div"));
});

James said...

pottedmeat: very nice. It did not occur to me to replace the original object with a modified one.

I liked the idea of modifying the prototype for $, but the mixin and replace solution is nice and can work in a generic case. It can even be collapsed down to a one-liner, so bonus points for that.

Anonymous said...

James,
setting non-standard __proto__ is quite simple. All you need is to use a "proxy" constructor (which Crockford advertises as a Object.beget method):

function F(){}
F.prototype = bar;
foo = new F;

// in Mozilla the following will be true
foo.__proto__ == bar;