An Example of Using True Inheritance with Plain JavaScript
UPDATE: The original article, written in 1999, focused on what is now referred to as "classical inheritance". This article is updated to also show how to accomplish the same behavior using "differential inheritance".
This article describes several techniques for implementing inheritance with JavaScript, including inheriting interfaces, implementations, and data from base classes. In this example, there's a base Animal
class that has a data member called lifespan
. Animal
has a virtual function called getSpeechText()
which should return a message that a particular animal will say when it is asked to talk. The generic Animal
class just says that it can't talk. Derived classes should override and return some text such as "bow wow" for dogs or "meow" for cats. The Animal
class also has a non-virtual function called talk()
which simply calls getSpeechText()
to get the text and then displays it to the console. The lifespan
field is initialized to a default value of 100
. It is a public
data member that any derived class can access and provide an initial value for (to change the default 100
set by the base). And finally, construction parameter is demonstrated by giving each Animal a name.
- The
getSpeechText()
method illustrates interface inheritance, where derived classes can override the behavior of a base class. - The
talk()
method illustrates implementation inheritance, where derived classes can simply inherit functionality from the base class (no override). - The
lifespan
data member illustrates data inheritance, where the derived classes inherit data referenced by a base class.
The example below will illustrate these features in C++ and JavaScript.
This example creates two classes derived from Animal
, called Dog
and Human
. Both override the getSpeechText()
method. Only the Dog
class changes the lifespan
data member (to 15). The Human
class uses the default 100 as initialized in the base class.
The "classical inheritance" method demonstrates a more traditional form of object-oriented programming, and will be familiar to those coming from a C++, Java or C# background. The "differential inheritance" method demonstrates a more prototypal technique to object inheritance, although "closures" are not used as they are not necessary to illustrate inheritance.
Both techniques use the fact that the prototype
property of a user-defined class can be changed to point to an object. Therefore, if we point the prototype
property of a derived class to a new instance of the base class, then the chain of responsibility will include this instance. This way, a member of the base class may be accessed via an instance of the derived class if the derived class doesn't have the same member defined. However, the "differential inheritance" example uses prototype indirectly through ECMAScript 5's support for Object.create.
The code uses the writeln()
utility function, which is included. It assumes the script is running on a very basic HTML page.
index.html
<html>
<body>
<div id='output'/>
<script src="Animal.js"></script>
</body>
</html>
There are also many command-line tools that you can use to run the JavaScript file, such as Node.js, js (jsshell) from Mozilla, jsdb and cscript.exe (JScript).
But first, let's first start with the C++ version so it's clear how we'd expect the classes to behave.
To compile the C++ version with Visual C++ installed, use the following command to create Animal.exe:
cl -EHsc Animal.cpp
C++ Animal.cpp
#include <iostream>
#include <string>
class Animal
{
public:
Animal(const std::string& name, int lifespan = 100)
: name( name ),
lifespan( lifespan )
{
}
void talk()
{
std::cout << getSpeechText() << std::endl;
}
virtual const char* getSpeechText()
{
return "Generic Animal doesn't talk";
}
const char* getName()
{
return name.c_str();
}
int lifespan;
std::string name;
};
class Dog : public Animal
{
public:
Dog(const std::string& name)
: Animal( name, 15 )
{
}
virtual const char* getSpeechText()
{
return "Bow wow";
}
};
class Human : public Animal
{
public:
Human(const std::string& name)
: Animal( name )
{
}
virtual const char* getSpeechText()
{
return "Hi there";
}
};
void main()
{
Dog fido("Fido");
Human bob("Bob");
std::cout << fido.getName() << "'s lifespan: ";
std::cout << fido.lifespan << " and talks by saying: " << std::endl;
fido.talk();
std::cout << bob.getName() << "'s lifespan: ";
std::cout << bob.lifespan << " and talks by saying: " << std::endl;
bob.talk();
}
JavaScript using Classical Inheritance: Animal.js
function classicalInheritanceExample() {
function Animal(name) {
this.lifespan = 100;
this.name = name;
}
Animal.prototype.getSpeechText = function () { return "Generic Animal doesn't talk"; };
Animal.prototype.getName = function () { return this.name; };
Animal.prototype.talk = function () { log(this.getSpeechText()); };
function Dog(name) {
Animal.call(this, name);
this.lifespan = 15;
}
Dog.prototype = new Animal();
Dog.prototype.getSpeechText = function () { return "Bow wow"; };
function Human(name) {
Animal.call(this, name);
}
Human.prototype = new Animal();
Human.prototype.getSpeechText = function () { return "Hi there"; };
var fido = new Dog("Fido"),
bob = new Human("Bob");
log(fido.getName() + "'s lifespan: " + fido.lifespan + " and talks by saying:");
fido.talk();
log(bob.getName() + "'s lifespan: " + bob.lifespan + " and talks by saying:");
bob.talk();
}
log("--------");
log("classicalInheritanceExample");
classicalInheritanceExample();
JavaScript using Differential Inheritance: Animal.js
function differentialInheritanceExample() {
var createAnimal, createDog, createHuman;
createAnimal = (function () {
var animalMethods = {
getSpeechText: function () { return "Generic Animal doesn't talk"; },
talk: function () { log(this.getSpeechText()); }
};
return function (name) {
var animal = Object.create(animalMethods);
animal.lifespan = 100;
animal.name = name;
return animal;
};
}());
createDog = (function () {
var dogMethods = {
getSpeechText: function () { return "Bow wow"; }
};
return function (name) {
var dog = createAnimal(name);
dog.lifespan = 15;
dog.getSpeechText = dogMethods.getSpeechText;
return dog;
};
}());
createHuman = (function () {
var humanMethods = {
getSpeechText: function () { return "Hi there"; }
};
return function (name) {
var human = createAnimal(name);
human.getSpeechText = humanMethods.getSpeechText;
return human;
};
}());
(function () {
var fido = createDog("Fido"),
bob = createHuman("Bob");
log(fido.name + "'s lifespan: " + fido.lifespan + " and talks by saying:");
fido.talk();
log(bob.name + "'s lifespan: " + bob.lifespan + " and talks by saying:");
bob.talk();
}());
}
log("--------");
log("differntialInheritanceExample");
differentialInheritanceExample();
Logging
The above code assumes there's a global log()
method. You can use something simple like this that will work for logging in most browsers' console window, or the Node.js console:
var log = console.log.bind(console);
Or you can use this generic routine that will work in most environments:
var log = (function () {
var logRef = null,
output = null;
try {
logRef = console;
try {
output = document.getElementById('output');
} catch (ignore) {
}
if (output) {
logRef = function () {
var args = Array.prototype.slice.call(arguments).join(", ");
console.log(args);
output.innerHTML += args + "<br/>";
};
} else {
logRef = console.log.bind(console);
}
} catch (e) {
logRef = null;
}
if (!logRef) {
try {
logRef = println;
} catch (ignore) {
}
}
if (!logRef) {
try {
logRef = print;
} catch (ignore) {
}
}
if (!logRef) {
try {
logRef = WScript;
logRef = function () {
WScript.Echo(Array.prototype.slice.call(arguments).join(", "));
};
} catch (e) {
logRef = null;
}
}
if (!logRef) {
throw "no logger";
}
return logRef;
}());
Output
The output from these samples should look something like this:
Fido's lifespan: 15 and talks by saying:
Bow wow
Bob's lifespan: 100 and talks by saying:
Hi there
Older ECMAScript Support
If you're using a pre-ECMAScript 5 browser (i.e., prior to Internet Explorer 9), you'll need to add the following to ensure the "
Object.create
" function exists:
if (typeof Object.create !== 'function') {
Object.create = function (o) {
function F() { return undefined; }
F.prototype = o;
return new F();
};
}
Main Differences between Classical and Differential Inheritance
The advantages of Classical Inheritance is that syntax seems more natural to the programmer:
var fido = new Dog("Fido");
fido.talk();
Dog
looks like a constructor here, like in other languages. The programmer calls new
to create an instance, and then call methods on that instance. One disadvantage is that it's not obvious here if Dog
() is to be used like a class or like a function that returns a Dog
instance, in which case new
shouldn't be called. In other languages, it you attempt to call <code>new
on anything other than a class, the compiler will catch it. But with JavaScript, the construct for a "class" and a function are the same, so you don't get any type of an error (runtime or compile-time) if you do this. The worst part is, if Dog
is meant to be a constructor, and the programmer doesn't call new
like they're supposed to, JavaScript calls Dog
() as a function, passing in the global context for the this
pointer. This means, as the constructor merrily uses the this
pointer to initialize its state, it's really assigning properties to the Web browser's window object. This type of error is difficult to detect early. The designers of JavaScript should have set this
to null or undefined when a constructor is called like a function (i.e. without new
.) Several solutions have been suggested to workaround this problem, such as naming conventions to make it obvious what is and isn't a constructor, and/or having each constructor verify that their this
pointer is not the global pointer. Using other tools such as Google's Closure Tools and Microsoft's TypeScript prevents these class of problems from occurring. However, the currently accepted best practice is to simply avoid using constructors for creating new objects altogether.
Differential Inheritance, as described in this article, is another solution to this problem. Rather than calling new
on Dog
, we simply supply a function that creates a Dog
object for us:
var fido = createDog("Fido");
fido.talk();
createDog
is basically a factory. Although not demonstrated here, this technique also allows for the use of closures for managing object state that's private
.
Simple performance experiment with jsPerf indicates that Differential Inheritance is actually faster. Try running the test on your browser now, which will make the overall collection better: http://jsperf.com/inheritanceperf/6.
History
- 9th February, 1999: Initial post (ref: link)
- 23rd August, 2010: Article updated (correct minor errors, spelling)
- 21st February 2014: Provided an example for both differential and classical Inheritance Example