Prototype

In classic Object Oriented Programming, we have class as templates and objects are instantiated from classes. In Javascript, oop is different.

In js, we have such things called prototype. We can create objects and delegate them to the linked prototype. prototype contains methods that are accessible to all objects that are linked to it, just like class methods in classes.

prototype itself is an object, it usually has this structure:

1
2
3
4
A.prototype = {
constructor: A,
__proto__: Object.prototype
};

Only functions(class function()) have .prototype, arrow functions and other objects don’t. All objects including functions have __proto__ property, which is linked to a .prototype object.

Instantiation using new

In js, functions have dual nature, it can be called as a ordinary function, and can also be stored and used as an object.
That is why functions can be used as constructors to instantiate new objects and serve as templates.

Constructor: used to create new objects, called whenever we use new.

So what happens when we use new:

  1. A new empty object is created {}
  2. The new objects __proto__ points to the constructor’s prototype:
    1
    2
    3
    4
    5
    6
    7
    const Person = function(name){
    this.name = name;
    }

    const emulisy = new Person("emulisy");

    console.log(emulisy.__proto__ === Person.prototype); //true
  3. Call the constructor with the created object.
  4. If the constructor returns an object, return it, otherwise return the created object.

Rewrite new

Now we know how new works, we can create our own myNew function:

Object.create(obj) creates a new object and points its __proto__ to the obj.

1
2
3
4
5
6
7
8
9
10
11
12
function myNew(constructorFunc, ...args){
const obj = Object.create(constructorFunc.prototype);

const result = constructorFunc.apply(obj, ...args)

if (result && (typeof result === 'object' || typeof result === 'function')) {
return result;
}

return obj;
}

The priority of this binding : new > explicit binding (apply, bind, call) > implicit binding (method call) > default binding

Prototypal inheritance and prototype chain

In oop, inheritance is mostly about reusing codes from template classes. In js, all instances can reuse methods from prototypes using the prototype chain.

When we want to use a variable, we look for the variable using scope chain, that is trying to find the variable in execution contexts inside the function and outside the function. Prototype chain is very similar to the scope chain.

1
2
3
4
5
6
7
8
9
10
function Person(name) {
this.name = name;
}

Person.prototype.sayHi = function() {
console.log('hi');
};

const p = new Person('Tom');

Assume we have a Person constructor and we created p from it. The prototype chain may look like this:

1
2
3
4
5
6
7
8
9
10
p
|
V
[[Prototype]] → Person.prototype
|
V
[[Prototype]] → Object.prototype
|
V
[[Prototype]] → null

Tips: we can get the prototype of the object using obj.[[prototype]] or obj.__proto__

  • [[prototype]]: The actual pointer used by the JavaScript engine to build the prototype chain, cannot be directly accessed.
  • __proto__: The public accessor to [[prototype]]
    The recommended way to access prototype is using Object.getPrototypeOf() and Object.setPrototypeOf()

When an object calls methods, JS looks for the method along the prototype chain until finding it or throwing an error. The end of all prototype chain is Object.prototype followed by null.

Same happens when we use instanceof to determine the type of an instance:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function myInstanceof(obj, constructorFunc){
if(typeof constructorFunc !== "function"){
throw new TypeError("Right-hand side of instanceof must be a function");
}

const targetProto = constructorFunc.prototype; //the target
let checkProto = obj.__proto__; //current proto

//going up the proto chain
while(checkProto !== null){
//if found
if(checkProto === targetProto){
return true;
}
checkProto = checkProto.__proto__;
}
// if not found
return false;
}

instanceof basically checks if the target prototype is in the object’s prototype chain.

typeof can only defer primitive types and functions, like typeof 1. All other variables will be regarded as “object”. It has nothing to do with prototype chain.

OOP before ES6 Classes

As we already know, we can create an instance from a function using new:

1
2
3
function Person(name) {
this.name = name;
}

Doing so, we basically use the function as constructor, and the this is bind to the new object.

When we want to add methods to the instance(or class), we can:

1
2
3
4
5
6
function Person(name) {
this.name = name;
this.sayHi = function () {
console.log("Hi")
}
}

But putting the method inside the constructor would mean that each instance have its own copy of sayHi, which is not memory efficient.

So the recommended way to add methods before ES6 is to use Prototype inheritance:

1
2
3
Person.prototype.sayHi = function () {
console.log("Hi");
}

When we call sayHi in Person, the js engine would look for the function along the prototype chain and find it in Person.prototype.

OOP after ES6 Classes

Many of the ES6 changes are actually syntactic sugar, and does not change the underlying implementation.

Class

In ES6, we have the Class to replace the ordinary function, which makes the oop more intuitive:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}

sayHi() {
console.log(`Hi, I'm ${this.name}`);
}
}

const p = new Person("Alice", 20);
p.sayHi();

  • constructor: called when using new on the class, it binds the this to the newly created object.

    when called without new, it will return an error.

  • methods: we no longer need to add methods using Person.prototype, just declaring it in the class and it will do it for us.

    Before ES6, methods in instances are enumerable, but we as developers expect that when we enumerate an object, we get its properties, not methods. In ES6 classes, methods are not enumerable:

    1
    2
    3
    4
    5
    const person = new Person("Emulisy");
    for(let key in p){
    console.log(key);
    }
    //we get name only, not sayHi.

Object.create(proto) and new:

When creating an instance with new, we link the prototype and call the constructor with this(the new object);

But with const obj = Object.create(Person.prototype), we only link the prototype, and stop the execution without calling the constructor.


Getters and Setters

1
2
3
4
5
6
7
8
9
get age() {
return this._age;
}

set age(value) {
if (value < 0) throw Error("Invalid age");
this._age = value;
}

We use get and set to define getters and setters. They define how we retrieve and rewrite a property

They behave like ordinary properties and cannot be called with ():

1
2
3
4
const person = new Person();
person.age(); //error
console.log(person.age); //get the age property, runs getter
person.age = 20; //rewrite the age property, runs setter

Static Methods

Methods like sayHi are instance methods, meaning they can only be called using an instance, not the class it self:

1
2
Person.sayHi(); //error
person.sayHi(); // "Hi"

These methods usually have access to properties, and their this points to the instance that calls them.

However, there are many methods that can be called without an instance, those are static/class methods:

1
2
3
static hello() {
console.log("hello");
}

We can only call hello using the Person class - Person.hello().

Beneath the static method:

When we declare instance method, we are actually adding it to the prototype of the instance:

1
2
3
 Person.prototype.sayHi = () => {
console.log("hi");
}

But when we declare static methods, we are adding it to the constructor function object:

1
2
3
Person.hello = () => {
console.log("hello");
}

The Person object have hello as a property:

1
Person.hasOwnProperty("hello"); //true

Arrow Function

In the class body, when we use a constant to store an arrow function, this function is not stored in the prototype chain as other methods. Instead, it will be stored in the instance as a property:

1
2
3
const greet = () => {
console.log("greet");
}

This is because arrow functions are not this binding. So doing so is like adding a field in constructor:

1
2
3
4
5
constructor() {
this.greet = () => {
console.log("greet"); //this points to the instance
}
}

Inheritance in ES6 classes

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Parent {
constructor(name) {
this.name = name;
}

sayHi() {
console.log(`Hi, I'm ${this.name}`);
}

static greet() {
console.log("Hello from Parent class");
}
}

class Child extends Parent {
constructor(name, age) {
super(name); // call Parent constructor
this.age = age;
}

sayAge() {
console.log(`I am ${this.age} years old`);
}

static greetChild() {
console.log("Hello from Child class");
}
}

1.extends: link the prototype chain:

1
Child.prototype.__proto__ = Parent.prototype;

Doing so we can use instance methods and properties from parent. But we still need to inherit static methods:

1
Child.__proto__  = Parent;

Now we can use static methods from Parent

2. super: call the parent constructor or methods

1
2
super(...) // calls Parent constructor with `this`
super.method() // calls Parent.prototype.method

So when we need to rewrite the parent’s but still use logic from parent, we can call the parent methods with super:

1
2
3
4
5
6
class Child extends Parent {
sayHi() {
super.sayHi(); // call Parent method
console.log(`and I am a child`);
}
}

Private fields and methods

Before ES6, if we want the field or method to be unprocessable from the outside, we need to hand-write a closure. But in ES6, we can define private fields and methods directly.

Private fields

Private fields must be declared with # :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class User {
#password;

constructor(password) {
this.#password = password;
}

checkPassword(input) {
return this.#password === input;
}
}

const u = new User("1234");

u.#password; // error
u.checkPassword("1234"); // true

  • It is only accessible inside the class body

  • It is not a property on the object, nor on the prototype. it is stored in internal slots.

  • It is not discoverable via reflection

Private Methods

It is also prefixed with a #:

1
2
3
4
5
6
7
8
9
class Counter {
#log() {
console.log("count changed");
}

inc() {
this.#log();
}
}

We can only call them inside the class body, and they are

  • NOT properties on the instance(like static methods)
  • NOT on User.prototype
  • NOT discoverable via reflection
  • NOT dynamically accessible(cannot be inherited)

Chaining Methods

When fetching promises, we might:

1
promise.then().catch().finally()

How can we chain the methods like that?

In order to use chained methods, we need to return the instance that in the methods:

1
2
3
4
5
6
7
8
9
10
11
12
class Counter {
inc() {
this.value++;
return this;
}

get() {
return this.value; // breaks chain
}
}

c.inc().inc().get(); // 2

this in instance methods points to the instance it self, so when we call inc(), we get the instance in return, and as long as it returns the instance itself, we can call it as a chain.