Click here to Skip to main content
11,576,293 members (51,352 online)
Click here to Skip to main content

True Inheritance with Plain-old JavaScript

, 24 Aug 2010 CPOL 20K 15 12
Rate this:
Please Sign up or sign in to vote.
This article describes a technique for making use of inheritance with JavaScript, including inheriting interface, implementation, and data from base classes.

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.

  1. The getSpeechText() method illustrates interface inheritance, where derived classes can override the behavior of a base class.
  2. The talk() method illustrates implementation inheritance, where derived classes can simply inherit functionality from the base class (no override).
  3. 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() {
    // class Animal
    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()); };

    // class Dog
    function Dog(name) {
        Animal.call(this, name);
        this.lifespan = 15;
    }
    Dog.prototype = new Animal();
    Dog.prototype.getSpeechText = function () { return "Bow wow"; };

    // class Human
    function Human(name) {
        Animal.call(this, name);
    }
    Human.prototype = new Animal();
    Human.prototype.getSpeechText = function () { return "Hi there"; };

    // Example
    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;

    // class Animal
    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;
        };
    }());
    
    // class Dog
    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;
        };
    }());

    // class Human
    createHuman = (function () {
        var humanMethods = {
            getSpeechText: function () { return "Hi there"; }
        };

        return function (name) {
            var human = createAnimal(name);
            human.getSpeechText = humanMethods.getSpeechText;
            return human;
        };
    }());

    // Example
    (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:

/*global console, println, print, WScript*/
// General purpose logging routine that works for client and server side scripts.
// 
// log(args) will print out args followed by a line-feed.
// 1. Works with all major browser:
//      When working in a browser, output goes to both console.log and the browser window.
//          Output will only go to the browser window if there's a
//          <div> in the body section with id='output'.  Example blank html:
//                  <html><body>
//                      <div id='output'/>
//                      <script src="script.js"></script>
//                  </body></html>
// 2. Works with common JavaScript command-line "shells":
//    - Node.js
//    - js (Mozilla's shell)
//    - jsdb
//    - JScript (use cscript.exe with wsh)
//
// Code below is optimized perform the environment detection only once/
//
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:
// This is needed for pre-ECMAScript 5 environments (pre-IE 9)
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 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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Kenneth Kasajian
United States United States
My interests mostly revolve around making machines do work for people. I'm a computer programmer, software architect, development manager, program manager and a computer programmer. I said programmer twice because I believe you need to be able to do the work yourself if you're going to direct others. I started my career creating software for abstract art, followed by work in embedded systems and HMI. In the 90s I created a successful product called Visual DLL and helped develop the Sales Force Automation product, Arsenal. I've often been involved in online communities, creating games, utilities, and collaboration software. I'm passionate about agile requirements management, acceptance testing through executable specification, and anything that will make developers more productive. My current role is Principal Scientist where I get to work on different technologies with an awesome team, to solve real-world practical problems. I'm Armenian, so talking is in my nature -- if you see me online or offline, say hi and we'll geek out about the latest tools and libraries. If you learned something cool recently (and you should if you're a lifelong learner), then I'd like to hear about it.

You may also be interested in...

Comments and Discussions

 
QuestionJSPerf test flawed Pin
Gjslick11-Sep-14 4:55
memberGjslick11-Sep-14 4:55 
AnswerRe: JSPerf test flawed Pin
Kenneth Kasajian13-Sep-14 11:46
memberKenneth Kasajian13-Sep-14 11:46 
Generalnamespacing Pin
dotnetCarpenter12-Feb-08 4:47
memberdotnetCarpenter12-Feb-08 4:47 
GeneralImproved Version Pin
SchaeferFFM10-Feb-08 7:12
memberSchaeferFFM10-Feb-08 7:12 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web04 | 2.8.150603.1 | Last Updated 24 Aug 2010
Article Copyright 2008 by Kenneth Kasajian
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid