Class-Based Inheritance in JavaScript
I use simulated class-based inheritance in JavaScript. My implementation is almost identical to and thanks to Kevin Lindsey's tutorial. This is the single best link on JavaScript I have ever found. The ideas allow for well structured, object-oriented programming in JavaScript.
Compared with Kevin's page, I've only changed some of names. Here is my version of Kevin's extend
function that works the magic.
function extend(subclass, superclass) {
function Dummy(){}
Dummy.prototype = superclass.prototype;
subclass.prototype = new Dummy();
subclass.prototype.constructor = subclass;
subclass.superclass = superclass;
subclass.superproto = superclass.prototype;
}
I will be using extend
in future examples.
The strengths of the class-based inheritence provided by extend
let me freely and guiltlessly think and work as though classes exist in JavaScript.
To avoid trouble with the pedantic JavaScript folks that will insist that classes do not exist in JavaScript, instead of saying "class-based inheritance in JavaScript" you can try calling it "constructor and prototype chaining."
JavaScript
I wanted to compare the syntax for JavaScript class-based inheritance with Java and Ruby: two languages that are respected for their pure object-oriented style.
For comparison here is Kevin's JavaScript example the way I write it.
function extend(subclass, superclass) {
function Dummy() {}
Dummy.prototype = superclass.prototype;
subclass.prototype = new Dummy();
subclass.prototype.constructor = subclass;
subclass.superclass = superclass;
subclass.superproto = superclass.prototype;
}
//------------------------------------------------------------------
function Person(first, last) {
this.first = first;
this.last = last;
}
Person.prototype.toString = function() {
return this.first + ' ' + this.last;
};
//------------------------------------------------------------------
function Employee(first, last, id) {
Employee.superclass.call(this, first, last);
this.id = id;
}
extend(Employee, Person);
Employee.prototype.toString = function() {
return Employee.superproto.toString.call(this) + ': ' + this.id;
};
//------------------------------------------------------------------
function Manager(first, last, id, department) {
Manager.superclass.call(this, first, last, id);
this.department = department;
}
extend(Manager, Employee);
Manager.prototype.toString = function() {
return Manager.superproto.toString.call(this) + ': ' + this.department;
};
Java
Here is Kevin's example written in Java. The main difference to notice is the Java super
calls are shorter because it is a built in feature of the Java language. But overall it appears as a line-by-line translation.
class Person {
String first, last;
Person (String first, String last) {
this.first = first;
this.last = last;
}
public String toString() {
return this.first + " " + this.last;
}
}
//------------------------------------------------------------------
class Employee extends Person {
int id;
Employee (String first, String last, int id) {
super(first, last);
this.id = id;
}
public String toString() {
return super.toString() + ": " + this.id;
}
}
//------------------------------------------------------------------
class Manager extends Employee {
String department;
Manager (String first, String last, int id, String department) {
super(first, last, id);
this.department = department;
}
public String toString() {
return super.toString() + ": " + this.department;
}
}
Ruby
The Ruby version below is more compact than the JavaScript or Java versions but all three are very similar.
class Person
def initialize(first, last)
@first = first
@last = last
end
def to_s
@first + ' ' + @last
end
end
#------------------------------------------------------------------
class Employee < Person
def initialize(first, last, id)
super(first, last)
@id = id
end
def to_s
super + ': ' + @id.to_s
end
end
#------------------------------------------------------------------
class Manager < Employee
def initialize(first, last, id, department)
super(first, last, id)
@department = department
end
def to_s
super + ': ' + @department
end
end
Comments
Have something to write? Comment on this article.
Kevin now has a blog post about the inclusion of YAHOO.extend
in v0.11. Congrats to Kevin!
Perl is not so pretty.
package Person;
sub new {
my $invocant = shift;
my $this = ref( $invocant) ? $invocant : bless({}, $invocant);
my ($first, $last) = @_;
$this->{first} = $first;
$this->{last} = $last;
return $this;
}
sub toString {
$this = shift;
return $this->{first} . ' ' . $this->{last};
}
#-------------------------------------------------------------------
package Employee;
our @ISA = ('Person');
sub new {
my $invocant = shift;
my $this = ref( $invocant) ? $invocant : bless({}, $invocant);
my ($first, $last, $id) = @_;
$this->SUPER::new($first, $last);
$this->{id} = $id;
return $this;
}
sub toString {
my $this = shift;
return $this->SUPER::toString() . ': ' . $this->{id};
}
#-------------------------------------------------------------------
package Manager;
our @ISA = ('Employee');
sub new {
my $invocant = shift;
my $this = ref( $invocant) ? $invocant : bless({}, $invocant);
my ($first, $last, $id, $department) = @_;
$this->SUPER::new($first, $last, $id);
$this->{department} = $department;
return $this;
}
sub toString {
my $this = shift;
return $this->SUPER::toString() . ': ' . $this->{department};
}
There is another approach to implement JavaScript inheritance - a "lazy" inheritance which has all benefits of "prototype" based approach like typed classes, but also eliminates necessity to declare external scripts in proper order and automatically resolves and loads (if necessary) dependencies to external scripts that contains related classes. This approach is supported by JSINER library - you can find more about it on http://www.soft-amis.com/jsiner/inheritance.html.
Perl doesn't have to be so long winded. I'd tend to do this with an object maker like Class::Std or Moose, but this is core Perl.
package Person;
sub new {
my $self = bless {}, shift;
@$self{qw(first last)} = @_;
$self;
}
sub toString {
my ($self) = @_;
join(" ", @$self{qw(first last)});
}
#-------------------------------------------------------------------
package Employee;
use base 'Person';
sub new {
my $self = shift->SUPER::new(@_[0,1]);
$self->{id} = $_[2];
$self;
}
sub toString {
my ($self) = @_;
join(": ", $self->SUPER::toString(), $self->{id});
}
#-------------------------------------------------------------------
package Manager;
use base 'Employee';
sub new {
my $self = shift->SUPER::new(@_[0,1,2]);
$self->{department} = $_[3];
$self;
}
sub toString {
my ($self) = @_;
join(": ", $self->SUPER::toString(), $self->{department});
}
"use strict,ses";
// Given ES3.1, here's a strict program that does
// http://peter.michaux.ca/articles/class-based-inheritance-in-javascript
// using Crock's objects-as-closures style adapted for single
// inheritance and nominal typing. In proposed SES, where functions
// are implicitly frozen on first use or escaping occurrence, the
// following code also follows capability discipline. Otherwise, in
// order to conform to capability discipline, all functions below
// would need to be explicitly frozen or replaced with ES-Harmony's
// proposed lambdas (which are implicitly frozen).
//------------------------------------------------------------------
// Infrastructure, given ES3.1
// For pre-ES3.1 support, see last section below
function copy(src) {
return Object.create(Object.getPrototypeOf(src),
Object.getOwnProperties(src));
}
function snapshot(src) {
return Object.freeze(copy(src));
}
function makeMaker(Super, initer) {
function Maker(var_args) {
var self = Object.create(Maker.prototype);
initer.apply(undefined, [self].concat(Array.slice(arguments, 0)));
return Object.freeze(self);
}
Maker.init = initer;
Maker.prototype = Object.freeze(Object.create(Super.prototype, {
constructor: {value: Maker}
}));
return Maker;
}
//------------------------------------------------------------------
var Person = makeMaker(Object,
function(self, first, last) {
self.toString = function() {
return first + ' ' + last;
};
});
//------------------------------------------------------------------
var Employee = makeMaker(Person,
function(self, first, last, id) {
Person.init(self, first, last);
var super = snapshot(self);
self.toString = function() {
return super.toString() + ': ' + id;
};
});
//------------------------------------------------------------------
var Manager = makeMaker(Employee,
function(self, first, last, id, department) {
Employee.init(self, first, last, id);
var super = snapshot(self);
self.toString = function () {
return super.toString() + ': ' + department;
};
});
//------------------------------------------------------------------
// Corresponding Infrastructure, given a pre-ES3.1 platform with __proto__
Object.create = function(parent) {
function F(){}
F.prototype = parent;
return new F();
}
Object.freeze = function(value) { return value; };
function copy(src) {
var result = Object.create(src.__proto__);
for (var k in src) {
if (Object.prototype.hasOwnProperty.call(src, k)) {
result[k] = src[k];
}
}
return result;
}
function snapshot(src) {
return copy(src);
};
function makeMaker(Super, initer) {
function Maker(var_args) {
var self = Object.create(Maker.prototype);
initer.apply(undefined, [self].concat(Array.slice(arguments, 0)));
return Object.freeze(self);
}
Maker.init = initer;
Maker.prototype = Object.create(Super.prototype);
Maker.prototype.constructor = Maker;
return Maker;
}
It's not clear to me that the snapshots above should preserve the implicit prototype of the original. So long as the super value is only used internally to do super calls, it doesn't matter. But if, for example, Manager.init leaks its super value, it is not good that this value would pass an "... instanceof Manager" test, since it does not represent a valid instance of a Manager. Neither in general is it a valid instance of Employee, since the "self" captured by its methods will eventually refer to a fully initialized instance of Manager.
Perhaps "super" should instead only be a frozen record inheriting directly from Object.prototype.
Have something to write? Comment on this article.
Good news!
Yahoo! just announced YUI v0.11 and they included a variation of Kevin's extend function as one of their three fundamental functions in the top level
YAHOO
namespace.Some links about it...