Skip to content

Class Implementation

Mention programming, and OOP (Object-Oriented Programming) usually comes to mind first. OOP is a design philosophy. If you think of a program as a person, objects are its organs, and the various methods inside each object are its cells.

In many languages — Python, C++, Java — classes are the blueprint for OOP. JavaScript introduced the class concept in ES6, but it's still just syntactic sugar built on ES5's prototype chain. Why didn't JS have classes from the beginning? Because Brendan Eich originally designed JS as a language to run in the browser and solve his immediate needs — he didn't introduce classes, instead using the prototype pattern for inheritance.

Speaking of prototypes, most people are familiar with prototype. It's actually a pointer to an object that holds the properties and methods shared by every instance of a given type. In plain terms: whatever lives on the prototype is inherited by all instances created from it. And class is implemented on top of this prototype. Let's look at the code:

javascript
// class
class Hello {
  constructor(x) {
    this.x = x;
  }
  greet() {
    console.log("Hello, " + this.x);
  }
}

let world = new Hello("world");
world.greet();

// es5
var Hello = (function() {
  function Hello(x) {
    this.x = x;
  }
  Hello.prototype.greet = function() {
    console.log("Hello, " + this.x);
  };
  return Hello;
})();
var world = new Hello("world");
world.greet();

The example above clearly shows how class syntax is implemented in ES5. Interestingly, a class's constructor is really just a constructor function. Under the hood, classes still rely on constructor functions and the prototype pattern to implement inheritance. When an instance is created, method calls on it actually call methods on the prototype. (Note: the body of a class defaults to strict mode, so you don't need use strict.)


Prototype Object

ES5 Syntax

Let's print out the world created with ES5:

javascript
var Hello = (function() {
  function Hello(x) {
    this.x = x;
  }
  Hello.prototype.greet = function() {
    console.log("Hello, " + this.x);
  };
  return Hello;
})();
var world = new Hello("world");

console.log(world);

prototype

Anyone familiar with JS prototypes knows that world.__proto__ points to the prototype object that world inherits. Interestingly, world.__proto__.constructor points back to the Hello constructor. Based on the prototype pattern, Hello.prototype === world.__proto__, meaning Hello.prototype.constructor also points back to Hello. Let's demonstrate with code:

javascript
var Hello = (function() {
  function Hello(x) {
    this.x = x;
  }
  Hello.prototype.greet = function() {
    console.log("Hello, " + this.x);
  };
  return Hello;
})();

var world = new Hello("world");

world.__proto__.constructor === Hello; //true
world.__proto__ === Hello.prototype; //true
Hello.prototype.constructor === Hello; //true

Class Syntax

Now let's print the world created with class syntax:

javascript
class Hello {
  constructor(x) {
    this.x = x;
  }
  greet() {
    console.log("Hello, " + this.x);
  }
}

let world = new Hello("world");

console.log(world);

hello

The output is basically the same as the ES5 version, except that constructor points to the Hello class itself — the function generated by class — rather than to a constructor function written out by hand:

javascript
class Hello {
  constructor(x) {
    this.x = x;
  }
  greet() {
    console.log("Hello, " + this.x);
  }
}

let world = new Hello("world");

world.__proto__.constructor === Hello; //true
world.__proto__ === Hello.prototype; //true
Hello.prototype.constructor === Hello; //true

The analysis above confirms that class is indeed syntactic sugar over ES5. Class inheritance is still based on the prototype chain, so I recommend thoroughly understanding JS's prototype pattern and prototype chain. That said, using class in production is recommended — it's more intuitive and easier to maintain.


Static Methods

A class is the prototype of its instances — all methods defined in a class are inherited by instances. Static methods, however, are not inherited by instances. How are they implemented?

javascript
// class
class Hello {
  constructor(x) {
    this.x = x;
  }
  static greet() {
    console.log("Hello, world");
  }
}

// es5
var Hello = (function() {
  function Hello(x) {
    this.x = x;
  }
  Hello.greet = function() {
    console.log("Hello, world");
  };
  return Hello;
})();

I imagine the reaction some people have when they see this:

yiwen

That's really how static methods are implemented? It almost seems too simple. But honestly, I think that's a good thing — it's very clear and straightforward.


Inheritance

Classes use the extends keyword for inheritance, which has an advantage over ES5's prototype-chain approach: it's clearer and more convenient. Let's see how ES5 implements inheritance:

javascript
// class
class Hello {
  constructor(x) {
    this.x = x;
  }
  static greet() {
    console.log("Hello, world");
  }
}

class Hi extends Hello {
  constructor(x) {
    super(x);
  }
}

// es5
var __extends =
  (this && this.__extends) ||
  (function() {
    var extendStatics = function(d, b) {
      extendStatics =
        Object.setPrototypeOf ||
        ({ __proto__: [] } instanceof Array &&
          function(d, b) {
            d.__proto__ = b;
          }) ||
        function(d, b) {
          for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
        };
      return extendStatics(d, b);
    };
    return function(d, b) {
      extendStatics(d, b);
      function __() {
        this.constructor = d;
      }
      d.prototype =
        b === null
          ? Object.create(b)
          : ((__.prototype = b.prototype), new __());
    };
  })();
var Hello = (function() {
  function Hello(x) {
    this.x = x;
  }
  Hello.greet = function() {
    console.log("Hello, world");
  };
  return Hello;
})();
var Hi = (function(_super) {
  __extends(Hi, _super);
  function Hi(x) {
    return _super.call(this, x) || this;
  }
  return Hi;
})(Hello);

Remember Ruan Yifeng's introduction to super in "ECMAScript 6 Primer"?

A subclass must call super() inside its constructor; otherwise, creating a new instance throws an error. This is because the subclass's own this object must first be shaped by the parent class's constructor, so it gains the same instance properties and methods as the parent. Only then does the subclass add its own instance properties and methods. If super() is never called, the subclass never gets a this object.

The code above illustrates this — ultimately, _super.call(this, x) is used to bind the instance's this.


Conclusion

This covers the basic implementation of class. There are many more properties and methods with even more interesting implementations — I'll share them when I have time. I recommend deeply understanding JS's prototype pattern and prototype chain. Whatever new syntax arrives in ES6, ES7, or beyond, it's all built on JavaScript's fundamental design patterns.