Skip to content

Instantly share code, notes, and snippets.

@Poetro
Last active August 29, 2015 14:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Poetro/0974f21c3b9b6fa16a1b to your computer and use it in GitHub Desktop.
Save Poetro/0974f21c3b9b6fa16a1b to your computer and use it in GitHub Desktop.
OOP in JavaScript

OOP in JavaScript

JavaScript is an Object Oriented language. It is a prototypical language, which means it handles inheritance by prototypes rather than classes. In JavaScript everything is an object or is automatically converted to one (except for null and undefined).

Primitives

JavaScript has 5 primitive types (number, boolean, string, null and undefined) that will automatically be wrapped into an object when methods are called on them, and then transformed back to primitive (except for null and undefined which does not have a wrapper objects):

"string".slice(2).replace("r", "k") === "king"; // true
(typeof 1) === "number"; // true

Primitives are immutable but can be wrapped by their wrapper object: Number, Boolean and String by using the new operator (or using the Object function) and passing the primitive value to it:

new Boolean(true) === true; // false
new Boolean(true).valueOf() === true; // true
(typeof (new Number(1))) === "object" // true
Object("string").valueOf() === "string" // true

The wrapped primitive is now a proper object and can be converted back to primitive by calling it's valueOf method. There is no valid use case for using the wrapper objects, so it is best to avoid them and take advantage of the automatic wrapping and unwrapping of the primitive types.

Objects

As I've said before, everything (except for the primitives) in JavaScript are objects (similar to Java). But compared to Java basically every object is a key-value store. It is easy to create objects by using the Object constructor or object literal (latter is preferred):

var o1 = new Object();
var o2 = {};

Unlike most languages you can add properties to object even after they are constructed, using either to dot syntax or using square brackets:

var o3 = {};
o2.poperty1 = "value1";
o2["property2"] = true;

The properties names are strings (and Symbols in ES6) and can store any value (primitives or objects). You can even declare the properties and the values in the object literal:

var o4 = { 
  property3: 1, 
  "continue": function () { 
    return null;
  }
};

In OOP we call methods functions that are attached to an object or class. In JavaScript object methods are just regular properties that are holding functions as the value.

Prototype chain

Each object has a prototype ([[Prototype]], not directly accessible in JavaScript, although some engines make it available through __proto__ property). If we are using the Object constructor or the object literal, then our object's [[Prototype]] is the Object's prototype object (and inherits some properties from it like toString and valueOf).

When you try go get a property of the object, then it first looks for it in the object's own properties. If the engine cannot find it, it will go up, in the prototype chain (by fetching the current object's [[Prototype]]) and look for the property there until it finds it or it reaches null in the prototype chain.

+---------------+  
|      null     |
+---------------+
        ^
        |
+---------------+
|     Object    |
+---------------+
| [[Prototype]] |
+---------------+
        |
       ...
        |
+---------------+
|    MyObject   |
+---------------+  
| [[Prototype]] |
+---------------+

Inheritance

In JavaScript inheritance is achived by the extending the prototype chain. By creating a new object, we add a new element to the top of the previously described chain. For example var o5 = {}; creates the following prototype chain:

+---------------+
|      null     |
+---------------+
        ^
        |
+---------------+
|     Object    |
+---------------+
| [[Prototype]] |
+---------------+
        ^
        |
+---------------+
|       o5      |
+---------------+
| [[Prototype]] |
+---------------+

You can override inherited properties (or add new ones) either in the object itself or anywhere in the prototype chain. Overriding it in the prototype chain means, that any object whose prototype chain includes that object will also gain that new behaviour. This behaviour of JavaScript can be considered as a huge strength, but also as a root of bugs.

Object.create

With Object.create it is easy to achieve inheritance:

var ObjectProto = {
  name: "ObjectProto",
  sayName: function() { 
    console.log("My name is: " + this.name)
  }
}

ObjectProto.sayName() // "My name is: ObjectProto"

var myObject = Object.create(ObjectProto);
myObject.name = "myObject";
myObject.sayName() // "My name is: myObject"

Although the code did not declare myObject.sayName it looks it up from ObjectProto and that is why it could be called.

The issue is, that we needed another line to override the name property of the prototype. This can be solved by passing a propertiesObject to the Object.create like:

var myObject2 = Object.create(ObjectProto, {
  name: {
    value: "myObject2", 
    enumerable: true, 
    writable: true, 
    configurable: true 
  }
});
myObject2.sayName() // "My name is: myObject2"

If the JavaScript implementation used did not implement Object.create it is easy to add a polyfill which implements the basic behaviour of it:

if (typeof Object.create != 'function') {
  Object.create = (function() {
    var Temp = function() {};
    return function (prototype) {
      if (arguments.length > 1) {
        throw Error('Second argument not supported');
      }
      if (typeof prototype != 'object') {
        throw TypeError('Argument must be an object');
      }
      Temp.prototype = prototype;
      var result = new Temp();
      Temp.prototype = null;
      return result;
    };
  })();
}

There is still the problem of automating the object creation. This is where constructors come to the rescue.

Constructors

Constructors are regular functions. When a function is called with the new operator it becomes a constructor function and will return an object (even if it returned a primitive or no value at all). We should design our constructor functions so that they look like constructors an behave like them. So if we intend to use a function as a constructor, it is best that it's name starts with an uppercase letter, to indicate that:

function Shape() {
  console.log("Constructor called");
}

var shape = new Shape; // () are optional when calling the constructor with `new` without any argument
// > "Constructor called"
console.log(typeof shape);
// > "object"

Just before the calling the constructor function, the JavaScript engine creates an object whose [[Prototype]] points to the constructor function's [[Prototype]]. This newly created object is set to be the this value (or context) within the function. If the function does not return anything, or does not return an object, the this will be automatically returned.

We could extend the object created by the constructor by adding properties to the prototype property of the constructor function even after they have been created:

Shape.prototype.move = function () {
  console.log("Shape moved");
};

All objects created by the constructor function will have these added properties in their prototype chain and because of this can be used.

Inheritance with constructors

We can combine the inheritance and the constructor function in JavaScript to get objects inherit properties from others. For this we will create a new constructor, replace the constructor function's prototype with the parent constructor's prototype and finally restore the constructor of the prototype to the original:

function Rectangle() {
}
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;

Now we are ready to use the constructor:

var rect = new Rectangle();
console.log(rect instanceof Rectangle); // true
console.log(rect instanceof Shape); // true
rect.move(); // "Shape moved"

Calling the superclass

The previous implementation has a slight issue. It does not call the parent's (or superclass's) constructor or when initializing the new object in it's constructor. It is easy to fix: we just need to call the parent constructor passing in the new object (and any other needed arguments).

function Rectangle() {
  Shape.call(this);
}

Having to remember what the superclass was is not elegant and also makes our code tightly coupled. We could make our reliance on the superclass a bit more dynamic by using some utility functions for the inhertance. Some libraries gives some convinience methods like util.inherits in Node.js and goog.inherits in Google Closure Library which makes inheritance and having a superclass seamless. If we are not using a library that already has it built in, we can easily implement it:

function inherits(ctor, superCtor) {
  ctor.super_ = superCtor;
  ctor.prototype = Object.create(superCtor.prototype, {
    constructor: {
      value: ctor,
      enumerable: false,
      writable: true,
      configurable: true
    }
  });
}

We can then use this to create the prototype chain and call the superclass's methods or constructor.

function Shape() {
  console.log("Constructor called");
}

Shape.prototype.move = function () {
  console.log("Shape moved");
};

function Rectangle() {
  Rectangle.super_.call(this);
}
inherits(Rectangle, Shape);

Rectangle.prototype.move = function () {
  console.log("Rectangle moved");
  Rectangle.super_.prototype.move.call(this);
};

If now we create a new Rectangle and if we call the rectangle's move method, it also calls the superclass's constructor and move method. As it removed the direct reference to Shape except for the inherits call, we can now easily swap out the Shape to something like Polygon with only changing one line of code in Rectangle.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment