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.

Peter Michaux July 11, 2006

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.

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...

Peter Michaux July 15, 2006

Kevin now has a blog post about the inclusion of YAHOO.extend in v0.11. Congrats to Kevin!

Peter Michaux February 16, 2007

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};
}
Andrew Sazonov April 10, 2007

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.

Andy Kirkpatrick September 7, 2007

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});
}
Mark S Miller February 12, 2008

"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;
}
Mark S. Miller February 13, 2009

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.