Object Oriented Programming in JS
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 | A.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:
- A new empty object is created
{} - The new objects
__proto__points to the constructor’s prototype:1
2
3
4
5
6
7const Person = function(name){
this.name = name;
}
const emulisy = new Person("emulisy");
console.log(emulisy.__proto__ === Person.prototype); //true - Call the constructor with the created object.
- 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 | function myNew(constructorFunc, ...args){ |
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 | function Person(name) { |
Assume we have a Person constructor and we created p from it. The prototype chain may look like this:
1 | p |
Tips: we can get the prototype of the object using
obj.[[prototype]]orobj.__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 usingObject.getPrototypeOf()andObject.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 | function myInstanceof(obj, constructorFunc){ |
instanceof basically checks if the target prototype is in the object’s prototype chain.
typeofcan only defer primitive types and functions, liketypeof 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 | function Person(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 | function Person(name) { |
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 | Person.prototype.sayHi = function () { |
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 |
|
constructor: called when using new on the class, it binds thethisto the newly created object.when called without new, it will return an error.
methods: we no longer need to add methods usingPerson.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
5const person = new Person("Emulisy");
for(let key in p){
console.log(key);
}
//we get name only, not sayHi.
Object.create(proto)andnew:When creating an instance with
new, we link the prototype and call the constructor withthis(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 | get age() { |
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 | const person = new Person(); |
Static Methods
Methods like sayHi are instance methods, meaning they can only be called using an instance, not the class it self:
1 | Person.sayHi(); //error |
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 | static 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 | const greet = () => { |
This is because arrow functions are not this binding. So doing so is like adding a field in constructor:
1 | constructor() { |
Inheritance in ES6 classes
1 | class Parent { |
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 | super(...) // calls Parent constructor with `this` |
So when we need to rewrite the parent’s but still use logic from parent, we can call the parent methods with super:
1 | class Child extends Parent { |
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 | class User { |
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 | class Counter { |
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 | class Counter { |
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.



