goog.base()
is a utility function in the Closure Library for invoking "superclass" methods. As you may recall from article on "Inheritance Patterns in JavaScript", the Closure Library uses the pseudoclassical inheritance pattern to emulate class hierarchies in JavaScript. As such, goog.base()
is used as a shorthand into to invoke a superclass's constructor function and methods.
How goog.base() Works
Before we can pick apart the implementation of goog.base()
, we must explore how inheritance relationships are established using goog.inherits()
in the Closure Library. In the inheritance article, there is a class named Phone
with a subclass named SmartPhone
. The inheritance relationship is established by the following function call:
goog.inherits(SmartPhone, Phone);
This establishes the appropriate link in the prototype chain between SmartPhone
and Phone
so that SmartPhone
may "inherit" methods from Phone
. Examining the implementation of goog.inherits()
, we also see that it adds a property named superClass_
to the subclass's constructor function that refers to the superclass's prototype:
/** * Inherit the prototype methods from one constructor into another. * @param {Function} childCtor Child class. * @param {Function} parentCtor Parent class. */ goog.inherits = function(childCtor, parentCtor) { /** @constructor */ function tempCtor() {}; tempCtor.prototype = parentCtor.prototype; childCtor.superClass_ = parentCtor.prototype; childCtor.prototype = new tempCtor(); childCtor.prototype.constructor = childCtor; };
Because goog.inherits()
adds a property named superClass_
to the constructor function of a subclass, an instance of a subclass can use this property to traverse its class hierarchy upward to find a superclass's constructor function and methods. This is effectively what goog.base()
does, which makes it possible to access a superclass without referencing it directly by name. For example, instead of calling the superclass constructor directly in SmartPhone
:
Phone.call(this, phoneNumber);
the equivalent can be done with goog.base()
:
goog.base(this, phoneNumber);
Similarly, instead of explicitly using the superClass_
property to access the superclass's getDescription()
method when overriding it in the subclass:
SmartPhone.superClass_.getDescription.call(this);
That call can be performed by goog.base()
to achieve the same result with fewer bytes of code:
goog.base(this, 'getDescription');
Although at first this may seem like a small victory, consider the advantages:
-
Fully qualified class names in the Closure Library are often long, so it is considerably easier to type
goog.base(this, 'disposeInternal')
than it is to typegoog.ui.Component.superClass_.disposeInternal.call(this)
. -
When renaming a class, superclass method invocations do not need to be updated if
goog.base()
is used. -
When changing the superclass, the call to the superclass constructor function does not need to be updated if
goog.base()
is used (unless the arguments to the new constructor function are different).
Although you may be eager to replace all of your long form superclass calls with goog.base()
, it is helpful to understand how it works first. Let's take a look at the implementation:
/** * @param {!Object} me Should always be "this". * @param {*=} opt_methodName The method name if calling a super method. * @param {...*} var_args The rest of the arguments. * @return {*} The return value of the superclass method. */ goog.base = function(me, opt_methodName, var_args) { var caller = arguments.callee.caller; if (caller.superClass_) { // This is a constructor. Call the superclass constructor. return caller.superClass_.constructor.apply( me, Array.prototype.slice.call(arguments, 1)); } var args = Array.prototype.slice.call(arguments, 2); var foundCaller = false; for (var ctor = me.constructor; ctor; ctor = ctor.superClass_ && ctor.superClass_.constructor) { if (ctor.prototype[opt_methodName] === caller) { foundCaller = true; } else if (foundCaller) { return ctor.prototype[opt_methodName].apply(me, args); } } // If we did not find the caller in the prototype chain, // then one of two things happened: // 1) The caller is an instance method. // 2) This method was not called by the right caller. if (me[opt_methodName] === caller) { return me.constructor.prototype[opt_methodName].apply(me, args); } else { throw Error( 'goog.base called from a method of one name ' + 'to a method of a different name'); } };
The first thing that goog.base()
does is check to see whether the caller is a constructor function by checking for the presence of the superClass_
property on arguments.callee.caller
. If so, it looks up the constructor function, calls it, and returns the result.
If the caller is not a constructor function, then it finds the method in the prototype chain of the instance (the me
argument) that is the same as the caller. Once the caller's place in the chain is found, the method of the same name is called the next level up in the prototype chain. This loop may appear complicated, but it is necessary to address some edge cases discussed later in this article.
Compiling Code with goog.base()
Those of you who are familiar with the Advanced mode of the Closure Compiler may be alarmed when you see that the method name is passed in as a string literal to goog.base()
, as you are aware that quoted and unquoted properties of objects are treated differently. Specifically, unquoted properties (such as method names) can be renamed by the Compiler whereas quoted properties (i.e., hardcoded strings, such as the second argument to goog.base()
in the case of a call to a superclass method) will not be renamed. How can goog.base()
work under compilation if methods are renamed but method names in goog.base()
calls are hardcoded?
The answer is that goog.base()
is processed in a special way by the Compiler when used in either Simple or Advanced mode. This work is done by ProcessClosurePrimitives.java (see the processBaseClassCall()
method). Basically, it programmatically replaces each call to goog.base()
with the equivalent long form. This happens early in the compilation process, before any renaming is done, so all references will ultimately be renamed consistently.
Limitations of goog.base()
If you have been using the long form of superclass invocation in the Closure Library, goog.base()
seems like a godsend when you first discover it. However, it has a couple of limitations that you should be aware of when using it in development. Fortunately, these limitations can be removed by either compiling your code in either Simple or Advanced mode with the Closure Compiler, or by replacing the problematic call to goog.base()
with the original long form.
Does Not Work with Arbitrary Superclass Methods
Because of howgoog.base()
is implemented, it can only be used to invoke the superclass method with the same name of the method from which goog.base()
is called. For example, the getDescription()
method of SmartPhone
is originally implemented as:
/** @override */ SmartPhone.prototype.getDescription = function() { return SmartPhone.superClass_.getDescription.call(this) + ' It can also send email messages.'; };
Using goog.base()
, it could be simplified as follows:
/** @override */ SmartPhone.prototype.getDescription = function() { return goog.base(this, 'getDescription') + ' It can also send email messages.'; };
But what if we wanted to use the superclass's getPhoneNumber()
method instead? You might expect to be able to do the following:
/** @override */ SmartPhone.prototype.getDescription = function() { return 'This is a phone with number ' + goog.base(this, 'getPhoneNumber') + '. It can also send email messages.'; };
Unfortunately, if you tried to invoke this method, you would get the following error:
goog.base called from a method of one name to a method of a different name
The error case in goog.base()
is reached because the following conditional inside the for
loop in goog.base()
is never satisfied:
if (ctor.prototype[opt_methodName] === caller)
One might wonder why that check is being done. For example, what if the for
loop were replaced with something simpler that just checked for the method in the superclass's prototype:
var args = Array.prototype.slice.call(arguments, 2); var ctor = me.constructor; var parentCtor = ctor.superClass_ && ctor.superClass_.constructor; if (parentCtor && parentCtor.prototype[opt_methodName]) { return parentCtor.prototype[opt_methodName].apply(me, args); } throw Error('No superclass method with name: ' + opt_methodName);
Although there are no comments in the implementation of goog.base()
to adequately explain why it is implemented the way it is, the unit test testBaseMethod()
in base_test.html provides a compelling example:
function A() {} A.prototype.foo = function(x, y) { return x + y; }; function B() {} goog.inherits(B, A); B.prototype.foo = function(x, y) { return 2 + goog.base(this, 'foo', x, y); }; function C() {} goog.inherits(C, B); C.prototype.foo = function(x, y) { return 4 + goog.base(this, 'foo', x, y); }; var d = new C(); d.foo = function(x, y) { return 8 + goog.base(this, 'foo', x, y); };
Now consider the case when d.foo(1, 0)
is called. When goog.base()
is called in d.foo
, the (ctor.prototype[opt_methodName] === caller)
condition will never be satisfied because the caller (d.foo
) does not exist on the prototype chain of d
's constructor, C
. The program will fall through to the condition (me[opt_methodName] === caller)
, which holds, as it is designed for this case where an ad-hoc property is used to override a method (rather than overriding a method by replacing it on the the prototype of a subclass). As a result, C.prototype.foo()
is called, as desired.
By comparison, if my oversimplified implementation were used instead, the goog.base()
call in d.foo()
would result in B.prototype.foo()
being called, which would not be correct.
Further, once C.prototype.foo()
is called, its own goog.base()
call should call B.prototype.foo()
, which is fairly straightforward. However, this calls goog.base()
again, though this time from B.prototype.foo()
. If me
is an instance of C
in my oversimplified implementation, then goog.base()
in B.prototype.foo()
will repeatedly call itself because me
invokes the overridden method of its parent class (which is B
) without regard to the caller. Fortunately, the true implementation of goog.base()
checks ctor.prototype[opt_methodName]
against caller
to protect against this type of infinite loop.
Because the logic required to make sure that goog.base()
works correctly is complex, it cannot be easily extended to support invoking a superclass method other than the one that is being overridden. Nevertheless, you can still call other superclass methods if you use the long form of the superclass method invocation:
/** @override */ SmartPhone.prototype.getDescription = function() { return 'This is a phone with number ' + SmartPhone.superClass_.getPhoneNumber.call(this) + '. It can also send email messages.'; };
Does Not Work under ES5 Strict
Note that the first line ofgoog.base()
is:
var caller = arguments.callee.caller;
According to Annex C of the ES5 specification, "Arguments objects for strict mode functions define non-configurable accessor properties named 'caller
' and 'callee
' which throw a TypeError
exception on access (10.6)." Therefore, when goog.base()
is called in a conformant implementation of ES5 strict, it will throw a TypeError
exception.
Because the Closure Compiler will rewrite all calls to goog.base()
when used in Simple or Advanced mode, goog.base()
will not be exercised in the compiled code, so it will be safe to use under ES5 strict.
Conclusions
goog.base()
is a convenient shorthand for invoking superclass methods in the Closure Library; however, there are some edge cases where it may cause subtle errors when it is not processed by the Closure Compiler. Because engineers frequently develop Closure Library code without compilation, it is important to be aware of these limitations. If such a problem is encountered, it is easily solved by replacing the call to goog.base()
with the equivalent long form of the superclass call. The end result will be the same after the code is compiled using either Simple or Advanced mode for production.