Classes to Prototypes: Object Oriented JavaScript for Non-JS Developers

Most object oriented languages use classes and class-based inheritence. JavaScript is of the minority that uses a different model, know as prototypal inheritence. JavaScript does not have classes; the closest thing to a class in JavaScript is a constructor and it’s prototype. Let’s start out by defining those terms and go from there.

Constructors

In JavaScript, a constructor is a function that is used to create objects of a certain type. For example, the built-in Array construtcor which, obviously, creates arrays. Constructors are called using the new keyword.

var arr = new Array();

Creating your own constructors is pretty easy, too.

function Apple(color) {
    this.color = color;
}

Here, we make a constructor called Apple that takes a parameter color. (In JavaScript, it is a common practice to capitalize the first letter of a constructor.) Now here comes a slightly confusing part. As you can see above, our Apple constructor is just a function; There is nothing special about it. Any function in JavaScript can be used as a constructor. When you call a function with the new keyword, it gets run as a constructor. This means that a new object is created and set as the this keyword. When the function is done running, the object is returned automatically (with no need for a return statement). Therefore, you can define properties and methods using this.

function Monkey(name) {
    this.name = name;
    this.speak = function(sayWhat) {
        console.log(this.name + ' the monkey says: ' + sayWhat);
    };
}

var bob = new Monkey('Bob');
console.log(bob.name);  // "Bob"
bob.speak('hi');  // "Bob the monkey says: hi"

What happens, though, if you call a constructor without the new keyword. In most cases, the this keyword defaults to the global object (window in the browser) which could cause some unexpected behavior.

var sarah = Monkey('Sarah');
console.log(sarah.name);  // TypeError: Cannot read property 'name' of sarah
console.log(name);  // "Sarah"

Prototypes

A prototype is an object that is shared between instances of a constructor. For example, we can go back to our Monkey constructor above; The speak method is something that is shared between all instances, so we can define it on the prototype.

function Monkey(name) {
    this.name = name;
}

Monkey.prototype.speak = function(sayWhat) {
    console.log(this.name + ' the monkey says: ' + sayWhat);
};

One major advantage to this is that the function only has to be defined once for all instances of the constructor. This saves both processing time and memory. It is also convenient to add methods after defining the constructor or even after creating instances.

var katie = new Monkey('Katie');

Monkey.prototype.foo = function() {
    console.log('foo');
};

katie.foo();  // foo

Inheritence

One constructor can inherit from another by setting the prototype of the child constructor to an instance of the parent.

function Macaque(name) {
    Monkey.apply(this, arguments);
}

Macaque.prototype = new Monkey();

Macaque.prototype.someMethod = function() {
    // ...
};

The apply method in JavaScript allows you to call a function while setting the scope. The first parameter gets set to the this keyword and the second parameter is an array (or array-like object, like the arguments object in this case) containing the parameters to pass to the function.

var joe = new Macaque('joe');
console.log(joe instanceof Macaque);  // true
console.log(joe instanceof Monkey);  // true

Everything in JavaScript inherits from the Object constructor. Every object has a property called constructor which, as you might have guessed, references the function which was used to create the object. When there are multiple inheritences like in this case (Object -> Monkey -> Macaque), the prototypes form a chain (Macaque.prototype.constructor === Monkey and Monkey.prototype.constructor === Object). When you try to read a property from an object, the lookup starts with direct properties of the object; If the property is not found, we look at the first prototype (Macaque) and work our way up the prototype chain until we find the property (or until we reach the end of the chain).

// Define A
function A() { }
A.prototype.a = function() {
    console.log('a');
};

// Define B which inherits from A
function B() { }
B.prototype = new A();
B.prototype.b = function() {
    console.log('b');
};

// Define C which inherits from B
function C() {
    this.bar = 'hi';
}
C.prototype = new B();
C.prototype.c = function() {
    console.log('c');
};

// Override "a"
C.prototype.a = function() {
    console.log('not a');
    // Call the super (or inherited) method
    A.prototype.a.apply(this, arguments);  // "a"
    // Or, more verbosely (and more dynamic)
    C.prototype.constructor.prototype.a.apply(this, arguments);  // "a"
    // Or, even more verbose and more dynamic
    this.constructor.prototype.constructor.prototype.a.apply(this, arguments);  // "a"
};

var foo = new C();
console.log(foo.bar);  // "hi"
foo.c();  // "c"
foo.b();  // "b"

Private Scope

Something you may have noticed is that all of the properties and methods are public; JavaScript has no private properties. There are a couple of different philosophies about how to handle this. One way is to just use public properties, usually prefixed by an underscore.

function Foo() {
    this._privateProperty = 'foo';
}

This way, your property is not protected, so it could be modified externally, but many say that this is enough and that we should trust others developers (or ourselves) to leave alone values that should not be tampered with. If you really want a value to be private, though, you can define it inside of a closure.

function Snake() {
    var age = 1;
    this.getAge = function() {
        return age;
    };
    this.growOlder = function() {
        age++;
    };
}

var mySnake = new Snake();
console.log(mySnake.getAge());  // 1
mySnake.growOlder();
console.log(mySnake.getAge());  // 2

A closure is a function scope (Snake, which contains age) that persists after the function is done executing because some inner function (getAge/growOlder) still has a reference to it. However, since you cannot access variables from outside of their scope, those persistant values stay private to everyone else. Variables declared like this have one major disadvantage, though. As I already said, only inner functions (or privileged functions) can access these variables, which means not functions that are defined on your prototype.

Snake.prototype.foo = function() {
    console.log(age);
};

mySnake.foo();  // ReferenceError

You end up having to compromise based on what is more important to you.

Static Members

This is the easiest part yet. Defining a static member is as simple as defining a property on the constructor itself.

function Banana() {
    // ...
}

// Create a static method
Banana.isBanana = function(obj) {
    return (obj instanceof Banana);
};

console.log(Banana.isBanana(new Banana()));  // true

It starts to ugly, though, if you need to have private, static members. You, again, can use underscores to prefix your “private” properties, but if you want actual privacy, you have to use another closure.

var Apple = (function() {
    
    var count = 0;
    
    // This is the actual constructor
    var Apple = function(color) {
        count++;
        this.color = color;
    };
    
    // This can actually access your private statics; yay!
    Apple.prototype.someMethod = function() {
        // ...
    };
    
    Apple.howMany = function() {
        console.log('There are currently ' + count + ' instances of Apple');
    };
    
    return Apple;
}());

So…

JavaScript very clearly does not use classical inheritence. Your first instinct may be to run and hide from prototypes, but they can be a very powerful way to write code. If you do decide to run away, there have been many (many… many) class implementations done in JavaScript; Ten seconds of googling should show some results, but I would strongly suggest getting to know JavaScript’s native inheritence model.

Share
blog comments powered by Disqus