Some time in 2013 as I was working through some JavaScript constructor inheritance tests, I found an example of static inheritance as an anti-pattern (meaning don't bother with it) in the transpiled code from a CoffeeScript tutorial.
The example is taken from
Programming in CoffeeScript by
Mark Bates, Addison-Wesley, pp. 147-150, where the
author shows that CoffeeScript does not support static inheritance through the
__super__
keyword.
However, the example contains more fundamental problems NOT specific to CoffeeScript, as I'll show with my own constructor.js examples.
Here's the CoffeeScript
class Employee
constructor: ->
Employee.hire @
@hire: (employee) ->
@allEmployees ||= []
@allEmployees.push employee
@total: (employee) ->
console.log "there are #{@allEmployees.length} employees."
@allEmployees.length
class Manager extends Employee
JavaScript generated by the transpiler
var Employee, Manager,
__hasProp = {}.hasOwnProperty,
__extends = function(child, parent) {
for (var key in parent) {
if (__hasProp.call(parent, key)) child[key] = parent[key];
}
function ctor() { this.constructor = child; }
ctor.prototype = parent.prototype;
child.prototype = new ctor();
child.__super__ = parent.prototype;
return child; };
Employee = (function() {
function Employee() {
Employee.hire(this);
}
Employee.hire = function(employee) {
this.allEmployees || (this.allEmployees = []);
return this.allEmployees.push(employee);
};
Employee.total = function(employee) {
console.log("there are " + this.allEmployees.length + " employees.");
return this.allEmployees.length;
};
return Employee;
})();
Manager = (function(_super) {
__extends(Manager, _super);
function Manager() {
return Manager.__super__.constructor.apply(this, arguments);
}
return Manager;
})(Employee);
After calling new Employee()
three times, Employee.total()
returns 3
.
After calling new Manager()
once, , Employee.total()
returns 4
.
Add the static inheritance
The author then adds a static Manager.total
method which is meant to
inherit-and-override the Employee.total
method
class Manager extends Employee
@total: ->
console.log "0 managers"
super
generating this JavaScript by transpiler
Manager.total = function total() {
return Manager.__super__.constructor.total.apply(this, arguments);
};
But the call to Manager.total()
throws an error
TypeError: this.allEmployees is undefined !
The problem here is that the this
being applied to the super constructor's
total()
method refers to the Manager constructor rather than a Manager
instance. CoffeeScript's __extends
method makes no attempt to determine
where super
is referenced, assuming that ANY calls to a super apply to this
.
And because Manager does NOT inherit the static allEmployees
array from
Employee, applying this
to total()
, rather than just calling total()
directly, produces the error.
Replacing this
manually with either Employee
or
Manager.__super__.constructor
clears up this problem
return Manager.__super__.constructor.total.apply(Employee, arguments);
return Manager.__super__.constructor.total.apply(
Manager.__super__.constructor, arguments);
but hand-cleaning CoffeeScript-generated JavaScript is not a winning scalable strategy.
But in constructor.js, this isn't this
While this specific example reveals a limitation in CoffeeScript, I found there was another flaw in the example when trying this out on constructor.js.
background
In constructor.js, an inheriting constructor makes a call to this.__super__()
that creates an instance of the super constructor, sets it to __super__
on the
inheriting object, and copies properties in the inheriting object. All further
references to __super__
point to the super object rather than a super
constructor.
Back to our problem…
Here's an Employee and Manager test fixture for constructor.js
function Employee() {
Employee.hire(this);
};
Employee.hire = function hire(employee) {
this.employees || (this.employees = []);
return this.employees.push(employee);
};
Employee.total = function total() {
return this.employees.length;
};
var Manager = Constructor.extend(Employee, {
constructor: function Manager() {
this.__super__(); // <= here's our constructor calling super
}
});
Manager.total = function total() {
return this.__super__.constructor.total(); // <= super_object method
};
After calling new Employee()
three times, Employee.total()
returns 3
.
After calling new Manager()
once, , Employee.total()
returns 4
.
However, when I iterated over the Employee.employees
array, the expected
Manager
entry at the end was NOT a Manager
at all, but an Employee
.
t.strictEqual(Employee.employees[1].constructor, Manager); // => not ok !?
It turns out the culprit is simply in the Employee
constructor, which takes no
data, but passes its current scope (whatever this
refers to) along to its
static hire()
method:
function Employee() {
Employee.hire(this);
};
Adding some logging to each constructor reveals the problem. This is OK:
function Manager() {
console.log(this.constructor.name) // Manager
this.__super__();
}
But this call to this.__super__()
in Manager
always delegates to the super
instance of Employee:
function Employee() {
console.log(this.constructor.name) // Employee !
Employee.hire(this);
}
...which passes itself to Employee.hire. This means that my __super__
implementation is doing work behind the scenes with two different objects rather
than a unified one.
fixing "static" "inheritance" for constructor.js
To get things to work in this scenario, avoid the work in the constructor, and
make a prototype method hire() in Employer, and move the Employee.hire(this);
statement from the constructor to the hire() method:
Employee.prototype.hire = function() {
Employee.hire(this);
}
...and in Manager, we can override that new method and access the Manager
instance's __super__
object and call hire.apply() passing the Manager instance
as the scope object to set the this reference properly:
Manager.prototype.hire = function() {
this.__super__.hire.apply(this);
}
Some test invocations later…
Call hire on new instances of each type:
(new Employee()).hire();
(new Manager()).hire();
t.strictEqual(Employee.total(), 2, 'should be 2 employees');
t.strictEqual(Manager.total(), Employee.total(), 'should inherit total()');
then test that Employee.employees[Employee.employees - 1]
correctly returns a
Manager instance:
t.strictEqual(Employee.employees[1].constructor, Manager, 'should be Manager');
fixing "static" "inheritance" in CoffeeScript
We'll do the same thing as with constructor.js. Move Employee.hire(this)
to a
new prototype method named hire
, and override Manager.prototype.hire
to call
its super instance object's hire
method, then replace the super
call in
Manager.@total
with a direct call to Manager.__super__.constructor.total()
:
};:
class Employee
constructor: ->
#Employee.hire @
hire: ->
Employee.hire(this)
///
class Manager extends Employee
@total: ->
console.log "call to super should error"
Manager.__super__.constructor.total()
hire: ->
super
These generate
function Employee() {}
Employee.prototype.hire = function() {
return Employee.hire(this);
};
///
function Manager() {
return Manager.__super__.constructor.apply(this, arguments);
}
Manager.total = function() {
console.log("call to super should error");
return Manager.__super__.constructor.total();
};
Manager.prototype.hire = function() {
return Manager.__super__.hire.apply(this, arguments);
};
This works, but it's not ideal. The employees array really should be owned by a collection handler rather be tacked on to a constructor - because JavaScript constructors are not classes. That's probably what led Jeremy Ashkenas to make Backbone Collections independent of the Model, View and Controller(Router) constructors.
nightcap recap
- avoid use of
super
in "static" methods in CoffeeScript - avoid "static" calls in the constructor
- move
super
calls from the constructor to prototype methods - make
super
calls from a static method point to a static method on__super__.constructor