Object Prototypes

Javascript object prototypes are a topic that’s very near and dear to my heart. Not nearly enough developers make use of the prototype object, or contemplate the overhead of prototype chaining of deeply nested object constructors. Some people use them, but not quite correctly, as in they don’t exactly know where to draw the line in the sand between what should be stored in an instance of an object, and what should be stored in that object’s prototype. This post will attempt to demystify javascript prototypes, and to make them a weapon of choice in your developer arsenal.

So how can we build larger-scale applications in the browser while at least trying to keep a handle on our memory usage? Object prototypes to the rescue! You may have seen references to an object’s prototype property either in someone’s code, or in a debugger, and not known what it was. Maybe you didn’t even pay attention to the fact that it was there, mixed in with a sea of other object properties. What is an object’s prototype? It’s a shared collection of properties and methods that all objects of a given type have access to. I’ll say that again. It’s a shared collection of properties and methods that all objects of a given type have access to. Because it’s shared, that means it takes up a single location in memory. It also means that messing with it will affect all objects that pull from that prototype definition. Used properly, the prototype can be a powerful construct. Used incorrectly, it can cause logical errors throughout your application.

var myConstructor = function () {};

myConstructor.prototype = {
    sharedProperty: "I am a sharedProperty!",
    sharedFunction: function () {
        console.log("sharedFunction was called!");
        return true;
    }
};

var foo1 = new myConstructor(),
    foo2 = new myConstructor(),
    foo3 = new myConstructor();

foo1.sharedFunction();
foo2.sharedFunction();
console.log(foo3.sharedProperty);

If you were to examine your console output, you would find 2 messages that “sharedFunction was called!” and 1 message that “I am a sharedProperty!” When javascript resolves object member references, it first looks to its instance-level data, and if it can’t find what it’s looking for there, it will begin traversing its prototype chain to see if it can find the reference there. So what kinds of properties are good candidates for being stored in an object’s prototype, and what should be left up to the object instances themselves? Anything that can be shared across all instances of an object should live in its prototype. Anything that’s required to define an object’s state should live in the instance.

Typically, you’ll find functions (accessors, mutation, and otherwise) stored in an object’s prototype, and individual properties stored in object instances. What do I mean by that? Think about if you had a custom Car object type. Some of the functions that could be shared across all instances of those objects might be things like honkHorn()startIgnition()stopIgnition()refuel(), etc. Properties that would define the state of a Car object might be things like isRunninghornSoundcolormakemodelyear, and fuelLevel. For any given car, you can assume that you can start and stop the ignition, honk the horn, and refuel it, though the implementations may vary (e.g. refuel() could behave differently for an electric car than it would for a gas-powered one), but each individual car can have a different make/model/year, fuel level, etc to define its state. An example implementation may look something like this:

var Car = function (color, make, model, year) {
    this.make = make;
    this.model = model;
    this.year = year;
    this.color = color;
    this.hornSound = "La Cucaracha";
    this.isRunning = false;
    this.fuelLevel = 0;
}

Car.prototype = {
    startIgnition: function () {
        this.isRunning = true;
    },
    stopIgnition: function () {
        this.isRunning = false;
    },
    refuel: function () {
        this.fuelLevel = 100;
    },
    honkHorn: function () {
        console.log(this.hornSound);
    }
};

var myCar = new Car("red", 1977, "AMC", "Gremlin");

Now you can create 100 instances of different types of cars, and at least as far as the prototype functions are concerned, there will only be one instance of them in memory. All of the identifying state of the Car objects themselves will eat up memory individually, but you’ve saved yourself the additional memory storage of 400 function expressions, if you had stored those 4 functions in each of your 100 Car instances instead of in Car.prototype property.

So why not just stick everything in the prototype? Remember, this is a shared memory location between all object instances. If we had all of the identifying state data as members of the prototype, each time we updated that data, we would be updating it for all of our instances that were looking to that prototype definition in memory. If we created 1 Car instance for a red 1977 AMC Gremlin and set those properties in the prototype, and then created a 2nd Car instance for a green 1969 Dodge Charger and set those properties in the prototype, our 1st Car instance would now think that it was also a green 1969 Dodge Charger by virtue of its data being stored in that same shared memory location. If you update the prototype for 1 instance, you update it for all instances.

So what are accessor and mutation functions? You might typically hear them referred to as getters and setters. They’re a way of providing access to an object’s internal properties without directly referencing them. Basically, it promotes a development style more consistent with traditional encapsulation, keeping private members private, and providing a facility to work with the members as needed, without directly altering those members. I’ll cover encapsulation more in another post. For now, you can just think of accessor functions as getters, and mutation functions as setters.

var Car = function (color, make, model, year) {
    this.setMake(make);
    this.setModel(model);
    this.setYear(year);
    this.setColor(color);
    this.setHornSound("la cucaracha");
    this.setIsRunning(false);
    this.setFuelLevel(0);
}
Car.prototype = {
    // Accessors
    getMake: function () {
        return this.make;
    },
    getModel: function () {
        return this.model;
    },
    getYear: function () {
        return this.year;
    },
    getColor: function () {
        return this.color;
    },
    getHornSound: function () {
        return this.hornSound;
    },
    getIsRunning: function () {
        return this.isRunning;
    },
    getFuelLevel: function () {
        return this.fuelLevel;
    },

    // Mutators
    setMake: function (make) {
        this.make = make;
    },
    setModel: function (model) {
        this.model = model;
    },
    setYear: function (year) {
        this.year = year;
    },
    setColor: function (color) {
        this.color = color;
    },
    setHornSound: function (hornSound) {
        this.hornSound = hornSound;
    },
    setIsRunning: function (isRunning) {
        this.isRunning = isRunning;
    },
    setFuelLevel: function (fuelLevel) {
        if (fuelLevel >= 0 && fuelLevel <= 100) {
            this.fuelLevel = fuelLevel;
        }
    },

    // Public functions
    startIgnition: function () {
        this.isRunning = true;
    },
    stopIgnition: function () {
        this.isRunning = false;
    },
    refuel: function () {
        this.fuelLevel = 100;
    },
    honkHorn: function () {
        console.log(this.hornSound);
    }
};

There are a whole slew of other things you can factor into your program design from an architectural perspective, like: whether to construct an object as a Factory; whether to allow access to any implementation details of your object’s internal makeup, or provide a strict public-facing API to use for interactions; how to retain private encapsulation of an object’s internal methods while providing for a testable interface; whether you want to directly couple object types together, use dependency injection, componentization, or some other construct to get your objects talking to one another. I’ll cover all of these things as time goes on, and will link to each post as it goes live.

One last thing I’d like to discuss is the prototype chain. If you’ve ever inspected a custom object in javascript, and looked at its prototype property, you may have noticed a property called __proto__. The __proto__ property is actually a pointer back to the previous constructor that this object’s prototype was created from.

var Vehicle = function () {};
Vehicle.prototype = {
    startIgnition: function () {
        this.isRunning = true;
    },
    stopIgnition: function () {
        this.isRunning = false;
    },
    refuel: function () {
        this.fuelLevel = 100;
    }
};

var Car = function () {};
Car.prototype = new Vehicle();

var Sedan = function () {};
Sedan.prototype = new Car();

var mySedan = new Sedan();

If you examine mySedan in a debugging utility, you’ll find that its __proto__ property points back to Car, and that Car’s __proto__ property points back to Vehicle, and Vehicle’s __proto__ property points back to the object definition we explicitly designated as its prototype property, which in turn points its __proto__ property back to the default Object constructor. Object points its __proto__ property to null, signaling that this is the end of the line, and no further references exist. So why do we need to know this? Well, each time you reference an object’s member data, the javascript runtime has to resolve that reference. It starts at the object’s instance-level members, and if it fails to find it there, it starts traversing the prototype chain, until it finds a reference to it at some point along the way, or it hits the null __proto__ reference in the base Object constructor, and resolves the member reference as undefined. This means that the more deeply nested your object prototype chains are, the further up that stack we have to potentially traverse in order to find a reference, or figure out that it’s actually undefined.

You can get around this if you know you’ll be dealing with a far-removed reference, by caching it as a local variable at the start of a function, and referencing that variable instead. You’ll take a slight hit in increased memory usage, but will save on CPU cycles if you’re making a lot of long-distance calls up that prototype chain. It’s up to the developer to determine when that tradeoff becomes worth it in terms of performance.

For now, I hope that the prototype property doesn’t seem to spooky or confusing, because once you get used to working with it, it’s really not. If you have any questions about something I didn’t cover, please feel free to ask in the comments below.

Leave a Reply

Your email address will not be published. Required fields are marked *