Published June 2, 2006 inInheritance Pattern,JavaScript
I use simulated class-based inheritance in JavaScript. My implementation is almost identical to and thanks toKevin 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'sextend 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 usingextend in future examples.
The strengths of the class-based inheritence provided byextend 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."
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;};Here is Kevin's example written in Java. The main difference to notice is the Javasuper 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; }}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 endend#------------------------------------------------------------------class Employee < Person def initialize(first, last, id) super(first, last) @id = id end def to_s super + ': ' + @id.to_s endend#------------------------------------------------------------------class Manager < Employee def initialize(first, last, id, department) super(first, last, id) @department = department end def to_s super + ': ' + @department endendHave 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 levelYAHOO namespace.
YAHOO.extend = function(subclass, superclass) { var f = function() {}; f.prototype = superclass.prototype; subclass.prototype = new f(); subclass.prototype.constructor = subclass; subclass.superclass = superclass.prototype; if (superclass.prototype.constructor == Object.prototype.constructor) { superclass.prototype.constructor = superclass; }};Some links about it...
Kevin now hasa blog post about the inclusion ofYAHOO.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 onhttp://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 belowfunction 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.