Skip to content

Class Implementation

When talking about programming, OOP (Object-Oriented Programming) comes to mind first. OOP is a design philosophy. If a program were a person, objects would be the organs, and the various operation functions inside objects would be the cells.

In many languages, the blueprint for OOP is based on classes — Python, C++, Java. JavaScript introduced the class concept in ES6, but it's still 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, everyone should be familiar with prototype. This property is actually a pointer to an object that contains properties and methods shared by all instances of a specific type. In plain terms: "this guy's stuff can be shared by his children and grandchildren." And class is implemented based on 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, class's constructor plays the role of a constructor function — it's essentially still using constructor functions and the prototype pattern to implement class inheritance. Instance method calls are actually calling methods on the prototype. (Note: class internals default to strict mode, so no need for 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

Friends familiar with JS prototypes know 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 ES5, except constructor points to the Hello class instead of the Hello constructor function:

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 fully proves that class is indeed ES5 syntactic sugar. 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 some people's reaction seeing this:

yiwen

That's how static methods are implemented? So simple and direct? Honestly, I think it's fine — very clear and straightforward.


Inheritance

Class uses the extends keyword for inheritance, which is an advantage over ES5's prototype chain approach: clear and 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"?

Subclasses must call the super method in the constructor method, otherwise an error will be thrown when creating a new instance. This is because the subclass's own this object must first be shaped by the parent class's constructor, getting the same instance properties and methods as the parent class, and then further processed with the subclass's own instance properties and methods. Without calling super, the subclass won't get a this object.

The code above fully illustrates this — ultimately, it uses the call function 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. No matter what new syntax comes out in ES6, ES7, or beyond, they're all built on JS's fundamental design patterns.