I went through the following example of Douglas Crockford's uber method
Oh, you poor lost soul. Notice that this function had its origin in 2002 (or earlier), when the language standard version was still ES3. This year, we will see ES9!
You can check the web archive to see the function slowly evolving to deal with all the edge cases that were discovered, and Crockford trying to fix them. (Notice that it still fails horribly if one of the involved methods throws an exception).
Needless to say, this is totally outdated. And borked.
Can someone please explain it?
I'll try to take a shot. Lets take the following code:
function A() { }
A.prototype.exampleMethod = function() {
console.log("top");
return "result";
};
function B() { }
B.inherits(A);
B.prototype.exampleMethod = function() {
console.log("parent");
return this.uber("exampleMethod");
};
function C() {
this.exampleMethod = function() {
console.log("instance");
return this.uber("exampleMethod");
}
}
C.inherits(B);
C.prototype.exampleMethod = function() {
console.log("prototype");
return this.uber("exampleMethod");
};
var x = new C();
console.log(x.exampleMethod());
This does should log instance
, prototype
, parent
, top
, result
- as one might have expected from a "super" call. How do these this.uber("exampleMethod")
invocations - the same method called with the same arguments on the same instance - achieve this? Horrible jugglery and trickery.
We see that this.uber
always calls the method that C.inherits(B)
had created. B.prototype.uber
is irrelevant. All calls will use the same d
object (referenced by closure), which stores the recursion depth per method name. p
is C.prototype
, and v
is B.prototype
.
The first call is from the instance method (created in the constructor). d.exampleMethod
is still 0 (or was just initialised to it as it didn't exist before), and we go to the else
branch to select the next method to call. Here it checks p[name] == this[name]
, i.e. C.prototype.exampleMethod == x.exampleMethod
, which is false when the instance (this
/x
) has an own (instance) method. So it selects the method from p
, not from v
, to call next. It increments the recursion count and calls it on the instance.
The second call is from the C.prototype
method. If this was the first call (as usual when having only prototype methods), d.exampleMethod
would be 0
. Again we'd go to the else
branch, but when there is no instance method we would have the comparison evaluate to true and would select v[name]
to call, i.e. the parent method we inherited. It would increment the recursion count and call the selected method.
The third call would be from the B.prototype
method, and d.exampleMethod
would be 1
. This actually already happens in the second call, because Crockford forgot to account for the instance method here. Anyway, it now goes to the if
branch and goes up the prototype chain from v
, assuming that the .constructor
property is properly set up everywhere (inherits
did it). It does so the for the stored number of times, and then selects the next method to call from the respective object - A.prototype.exampleMethod
in our case.
The counting must be per-methodname, as one could attempt to call a different method from any of the invoked super methods.
At least that must have been the idea, as obviously the counting is totally off in case there is an instance method. Or when there are objects in there prototype chain that don't own the respective method - maybe also that was a case that Crockford attempted but failed to deal with.