You see, normal functions have names (sort of):
function foo () { }
In Firefox, one can even get foo.name to find the value "foo". Why would one do this you ask? There are several useful applications for such information. For example, we can walk the callstack:
function stackdump ()
{
var a = [];
for (var f = arguments.callee.caller; f; f = f.caller)
a.push(f.name);
return a;
}
But this breaks down with $namespace as we currently have it. The problem is that $namespace copies functions from an object literal to the target namespace. Functions declared in this manner have no name:
$namespace("foo.bar", {
func : function () { }
});
So, while user code knows this function as foo.bar.func, the JS function object does not know its own name. My first idea was to set the name property in the copy loop. The name is known at the time the function object is copied after all.
There are really only two problems with my first idea: it didn't work and it really wouldn't have helped anyway. Either of these alone would probably be enough to put an end to the pursuit, but it is worth a bit of explanation. For the first problem, Firefox won't let you set the name property. Second problem: the name property is only present in Firefox (like I said above), so providing it only in this case does not give a universal answer for the name of a function.
What is needed is a way to solve the problem of name for every function object. So how can we make something available to all function objects, even ones we've never encountered before? The answer lies in the magic powers of JavaScript prototypes.
The prototypal inheritance nature of JavaScript makes it one of the very few truly Object Oriented Programming Languages. Most "OO" languages like C++ and Java use the concept of a class as distinct from an object. The class serves as a kind of static, built-in indirection mechanism. All objects of a given class indirectly reference their class definition. In Java, the class has a run-time presence in the form of an object of type Class. These class objects provide limited, read-only access to the class declaration at run-time. In C++, there is no real run-time representation of a class at all (even given the so-called RTTI feature of the language).
Background: In JavaScript, every object implicitly points to another object called its prototype object. Whenever an object property is requested via the "." operator, that object is first examined to see if the property exists (as one would expect). Then things get interesting. If the property does not exist, the prototype object is also searched for the property. This repeats until the prototype chain is exhausted or a value is found, whichever comes first. The prototype for an object is set when the object is created using the new operator. Since there are no classes in JavaScript, functions serve as constructors. When an object is created using a particular function as a constructor, that function object's prototype property becomes the newly created object's prototype object. Note, the prototype object is not stored as the prototype property of the new object. The prototype for an object is stored internally and is not (portably) accessible. There are several pre-defined constructors: Object, Array and Function being the most useful. All objects terminate their prototype chain with Object.prototype. All array objects pass through Array.prototype and then on to Object.prototype. Likewise, all function objects have Function.prototype followed by Object.prototype in their prototype chain. In other words, we can add properties to all objects by adding to Object.prototype.
Before you get tempted to run out and add stuff to either Object.prototype or Array.prototype let me warn you of the EVIL that will ensue if you do so. Consider the following for loop:
for (var x in [ a, b, c ])
...
The author's intent is fairly clear. The definition of for (x in a), however, is such that it will iterate over all properties, including those added to Array.prototype and/or Object.prototype! Shudder. As you might imagine others have written about this issue. The best one can say is that modifying these prototype objects produces code that "doesn't play well with others". My advise is that for your own sanity, but more for those who use your code, stay away from this technique.
With that rant out of the way, I feel like I can move on. Fortunately for us, we can modify Function.prototype and have none of these problems. We need two methods:
// called as function objects are added to a namespace via $namespace:
Function.prototype.$namespace = function (ns, name)
{
var fn = ns.$namespace.fullname ? (ns.$namespace.fullname + ".") : "";
this.$meta = { fullname : fn + name, name : name, namespace : ns };
}
Function.prototype.getName = function ()
{
if (!this.$meta)
{
var name = this.name || this.toString().match(/function (\w*)/)[1];
name = name || "~anonymous~";
this.$meta = { fullname : name, name : name, namespace : $namespace.global };
}
return this.$meta.fullname;
}
If you recall, the $namespace fuction calls $namespace if it is a function property of the copied object. That is to say, if we provide a $namespace method, it will be called when/if the function is added to a namespace.
After all that then, the method getName is now defined for all function objects. Here now is the portable way to get the name of a function object:
function foo () { }
var s = foo.getName();
And the modified method to walk the callstack:
function stackdump ()
{
var a = [];
for (var f = arguments.callee.caller; f; f = f.caller)
a.push(f.getName());
return a;
}
No comments:
Post a Comment