Inheritance Patterns in JavaScript
The Closure Library makes use of the pseudoclassical inheritance pattern, which is particularly compelling when used with the Closure Compiler. Those of you who have read JavaScript: The Good Parts by Douglas Crockford may use the functional pattern for inheritance that he espouses.
Crockford appears to object to the pseudoclassical pattern
because: "There is no privacy; all properties are public.
There is no access to super
methods...Even worse, there is a
serious hazard with the use of constructor functions. If you forget to use the
new
prefix when calling a constructor function, then
this
will not be bound to a new object...There is no compile
warning, and there is no runtime warning."
This article discusses the advantages of the pseudoclassical pattern over the functional pattern. I argue that the pattern used by the Closure Library paired with the Closure Compiler removes existing hazards while I also examine the hazards introduced by the functional pattern (as defined in The Good Parts). First let me demonstrate what I mean by the functional pattern.
Example of the functional pattern
The following is an example in the style of the functional pattern for inheritance as explained in Douglas Crockford's JavaScript: The Good Parts. It contains the definition for aphone
type as well as a subtype
smartPhone
.
var phone = function(spec) { var that = {}; that.getPhoneNumber = function() { return spec.phoneNumber; }; that.getDescription = function() { return "This is a phone that can make calls."; }; return that; }; var smartPhone = function(spec) { var that = phone(spec); spec.signature = spec.signature || "sent from " + that.getPhoneNumber(); that.sendEmail = function(emailAddress, message) { // Assume sendMessage() is globally available. sendMessage(emailAddress, message + "\n" + spec.signature); }; var super_getDescription = that.superior("getDescription"); that.getDescription = function() { return super_getDescription() + " It can also send email messages."; }; return that; };Instances of each of these types could be created and used as follows:
var myPhone = phone({"phoneNumber": "8675309"}); var mySmartPhone = smartPhone({"phoneNumber": "5555555", "signature": "Adios"}); mySmartPhone.sendEmail("noone@example.com", "I can send email from my phone!");
Example of the pseudoclassical pattern
Here is the same logic as the previous example, only written using Closure's style and coding conventions.goog.provide('Phone'); goog.provide('SmartPhone'); /** * @param {string} phoneNumber * @constructor */ Phone = function(phoneNumber) { /** * @type {string} * @private */ this.phoneNumber_ = phoneNumber; }; /** @return {string} */ Phone.prototype.getPhoneNumber = function() { return this.phoneNumber_; }; /** @return {string} */ Phone.prototype.getDescription = function() { return 'This is a phone that can make calls.'; }; /** * @param {string} phoneNumber * @param {string=} signature * @constructor * @extends {Phone} */ SmartPhone = function(phoneNumber, signature) { Phone.call(this, phoneNumber); /** * @type {string} * @private */ this.signature_ = signature || 'sent from ' + this.getPhoneNumber(); }; goog.inherits(SmartPhone, Phone); /** * @param {string} emailAddress * @param {string} message */ SmartPhone.prototype.sendEmail = function(emailAddress, message) { // Assume sendMessage() is globally available. sendMessage(emailAddress, message + '\n' + this.signature_); }; /** @override */ SmartPhone.prototype.getDescription = function() { return SmartPhone.superClass_.getDescription.call(this) + ' It can also send email messages.'; };Similarly, here is an example of how these types could be used:
goog.require('Phone'); goog.require('SmartPhone'); var phone = new Phone('8675309'); var smartPhone = new SmartPhone('5555555', 'Adios'}; smartPhone.sendEmail('noone@example.com', 'I can send email from my phone!');
Drawbacks to the functional pattern
Instances of types take up more memory
Every timephone()
is called, two new functions are created
(one per method of the type).
Each time, the functions are basically the same, but they are bound to
different values.
These functions are not cheap because each is a closure that maintains a
reference for every named variable in the enclosing function in which the
closure was defined. This may inadvertently prevent objects from being garbage collected,
causing a memory leak.
The Closure Library defines goog.bind()
and goog.partial()
in base.js
to make it easier to create closures that only maintain the references they need,
making it possible for other references to be removed when the enclosing function
exits.
This is not a concern when Phone()
is called because of how it takes
advantage of prototype-based inheritance. Each method is defined once on
Phone.prototype
and is therefore available to every instance of
Phone
. This limits the number of function objects that are created
and does not run the risk of leaking memory.
Methods cannot be inlined
When possible, the Compiler will inline methods, such as simple getters. This can reduce code size as well as improve runtime performance. Because the methods in the functional pattern are often bound to variables that cannot be referenced externally, there is no way for the Compiler to rewrite method calls in such a way that eliminates the method dispatch. By comparison, the following code snippet:// phone1 and phone2 are of type Phone var caller = phone1.getPhoneNumber(); var receiver = phone2.getPhoneNumber(); operator.createConnection(caller, receiver);could be rewritten to the following by the Compiler:
operator.createConnection(caller.phoneNumber_, receiver.phoneNumber_);
Superclass methods cannot be renamed (or will be renamed incorrectly)
When the Closure Compiler is cranked up to 11, one of the heuristics it uses for renaming is that any property that is not accessed via a quoted string is allowed to be renamed, and the Compiler will do its best to rename it. All quoted strings will be left alone. It is not required to use the Compiler with this aggressive setting, but the potential reduction in code size is too big to ignore.
From the phone
example, getDescription
is used both as
a property defined on that
and as a string literal passed to
that.superior()
. If aggressive renaming were turned on in the Compiler,
getDescription
would have to be used as a quoted string throughout
the codebase so that it did not get renamed. (It could also be declared as an extern,
which is what prevents built-in method names, such as toString()
from being renamed, but because there is no function to associated the method
with, it would have to be declared as an extern on Object.prototype
.)
Remembering to refer to it via a string literal throughout the codebase is a
bear and precludes the benefits of aggressive renaming.
(To be fair, some of the constructs in the candidate spec for EcmaScript 5,
such as Object.defineProperty()
, have similar issues.
The solution will likely be to add logic to the Compiler to treat
Object.defineProperty()
in a special way.
The same could be done for superior()
if one were so motivated.)
Types cannot be tested using instanceof
Because there is no function to use as the constructor, there is no appropriate
argument to use with the right side of the instanceof
operator to
test whether an object is a phone
or a smartPhone
.
Because it is common for a function to accept multiple types in JavaScript,
it is important to have some way to discern the type of the argument that was
passed in.
An alternative would be to test for properties that the desired type in question may have, such as:
if (arg.sendEmail) { // implies arg is a mobilePhone, do mobilePhone things }There are two problems with this solution. The first is that checking for the
sendEmail
property is only a heuristic -- it does not guarantee
that arg
is a mobilePhone
. Perhaps there is also a
type called desktopComputer
that also has a sendEmail
method that would also satisfy the above test. The second problem is that this
code is not self-documenting. If the conditional checked
if (arg instanceof MobilePhone)
, it would be much clearer what was being
tested.
Encourages adding properties to Function.prototype
and
Object.prototype
In Chapter 4 of The Good Parts,
Crockford introduces Function.prototype.method
and uses it in Chapter 5 via Object.method
to add a property
named superior
to Object.prototype
to aid in creating
superclass methods. Adding properties to fundamental prototypes makes it harder
for your code to play nicely with other JavaScript libraries on the page if
both libraries modify prototypes in conflicting ways.
The Google Maps team learned this the hard way when they decided to add
convenience methods to Array.prototype
, such as insertAt
(incidentally, this is exactly what Crockford does in Chapter 6).
It turned out that many web developers were using for (var i in array)
to iterate over the elements of an array (as opposed to using
for (var i = 0; i < array.length; i++)
as they should have been).
The for (var i in array)
syntax includes properties added to
Array.prototype
as values of i
, so bringing in
the Google Maps library would break the code on those web pages. Rather than
trying to change web developers' habits, the Maps team changed their library.
It is for reasons such as these that
array.js
in Closure is a collection of array utility functions that take an array as their first
argument rather than a collection of modifications to Array.prototype
.
For those of you who do not own Crockford's book, here is the code in question. Try running the following and see what happens:
// From Chapter 4. Function.prototype.method = function(name, func) { this.prototype[name] = func; return this; }; // From Chapter 5. Object.method('superior', function(name) { var that = this, method = that[name]; return function() { return method.apply(that, arguments); }; }); // Create a new object literal. var obj = { "one": 1, "two": 2 }; // Enumerate the properties of obj. for (var property in obj) alert(property);Instead of enumerating two properties in
obj
, three are listed:
one
, two
, and superior
. Now every
object literal in your program will also have a property named superior
.
Though it may be handy for objects that represent classes, it is inappropriate
for ordinary record types.
Makes it impossible to update all instances of a type
In the Closure model, it would be possible to add a field or method to all instances of a type at any point in the program by adding a property to the function constructor's prototype. This is simply not possible in the functional model.Although this may seem like a minor point, it can be extremely useful when developing or debugging a system to redefine a method on the fly (by using a REPL such as Chickenfoot or the Firebug console). This makes it possible to put probes into a running system rather than having to refresh the entire web page to load changes.
Naming newly created objects is awkward
This may be more of a personal preference, but the "capitalize constructor functions" convention makes it fairly intuitive to name a newly created object. From the pseudoclassical example, we have:var phone = new Phone('8675309');where the name of the variable matches that of the constructor, but has a lowercase letter. If the same thing were done in the functional case, it could cause an error:
function callJenny() { var phone = phone('8675309'); makeCall(phone.getPhoneNumber()); }Here the local variable
phone
shadows the function phone()
, so
callJenny()
will throw an error when called because phone
is
bound to undefined
when it is applied to '8675209'
.
Because of this, it is common to add an arbitrary prefix, such as my
,
to the variable name for newly created objects when using the functional pattern.
Results in an extra level of indentation
Again, this may be more of a personal preference, but requiring the entire class to be defined within a function means that everything ends up being indented one level deeper than it would be when using the Closure paradigm. I concede that this is how things are done in Java, and C# even suggests adding an extra level of depth for good measure with its namespaces, so maybe there's some prize for hitting the tab key a lot that no one told me about. Regardless, if you have ever worked someplace (like Google) where 80-character line limits are enforced, it's nice to avoid line-wrapping where you can.Potential objections to the pesudoclassical pattern
Won't horrible things happen if I forget the new
operator?
If a function with the @constructor
annotation is called without
the new
operator, the Closure Compiler will emit an error.
(Likewise, if the new
operator is used with a function that does
not have the annotation, it will also throw an error.) Crockford's objection
that there is no compile-time warning no longer holds! (I find this odd
because Crockford could have created his own annotation with an identical check
in JSLint.)
Didn't Crockford also say I wouldn't have access to super
methods?
Yes, but as the example above demonstrates, a super class's methods are accessible
via the superClass_
property added to a constructor function.
This property is added as a side-effect of calling
goog.inherits(SmartPhone, Phone)
. Unlike the functional example
with super_getDescription
, super class accessors do not need to be
explicitly created in Closure.
Won't all of the object's properties be public?
It depends on what you mean by "public." All fields and methods of an object marked with the@private
annotation will be private in the sense
that the Compiler can be configured to reject the input if a private property is
being accessed outside of its class.
So long as all of the JavaScript in your page is compiled together, the
Compiler should preclude any code paths that would expose private data.
Won't declaring SomeClass.prototype
for each method and field
of SomeClass
waste bytes?
Not really. The Compiler will create a temporary variable for
SomeClass.prototype
and reuse it. As the Compiler works today,
it is admittedly most compact to write things in the following style:
SomeClass.prototype = { getFoo: function() { return this.foo_; }, setFoo: function(foo) { this.foo_ = foo; }, toString: function() { return '<foo:' + this.getFoo() + '>'; } };However this style has some drawbacks. Doing the above introduces an extra level of indenting and makes reordering methods more tedious because extra care is required to ensure that the last property declared does not have a trailing comma and that all of the other properties do. I think that it is reasonable to expect the Compiler to produce output more similar to the above in the future, but that the recommended input will continue to match the style exemplified in the pseudoclassical example.
I don't need static checks -- my tests will catch all of my errors!
Let's be honest -- do you write tests? And if you are writing tests, how much confidence are they giving you about your code's correctness? As I've discussed previously, existing tools for testing web applications make it difficult to write thorough tests. So even if you have taken the time to write tests, it is unlikely that they exercise all of your code. (Lack of good tools for measuring code coverage by JavaScript tests contributes to the problem.)By comparision, the Closure Compiler examines your entire program and provides many compile-time checks that can help you find errors before running your code. If Douglas Crockford claims that JSLint will hurt your feelings, then the Closure Compiler will put you into therapy.
Good grief!"