Inheritance and Encapsulation in JS land

Photo by Sigmund on Unsplash

Inheritance and Encapsulation in JS land

Proto_types in JS for season developers

A prototype is an object to which the search for a particular property can be delegated. Prototypes are a convenient means of defining properties and functionality that will be automatically accessible to other objects. Prototypes serve a similar purpose to that of classes in classical object-oriented languages. Indeed, the main use of prototypes in JavaScript is in producing code written in an object-oriented way, similar to, but not exactly like, code in more conventional, class-based languages such as Java or C#.

More importantly, we'll discover why it's important to having prototypes in JS land

Generate Object using functions

In JavaScript, objects are collections of named properties with values. For example, we can easily create new objects with object-literal notation

const obj = {
    prop1 : 'a',
    prop2 : ()=> {},
    prop3 : {}
}

When developing software, we strive not to reinvent the wheel, so we want to reuse as much code as possible

function userCreation(name,score) {
  const newUser = {};
  newUser.name = name;
  newUser.score = score;
  newUser.increment = ()=> {
    newUser.score++;
  }
  return newUser;
}
💡
We do two good things: we avoid creating a repeat piece of code to manually assign variables to an object, and we bundle data and functions together in one place. So we can use it all together

The problem is that every time the user calls this function to create a JS object, it will create the same objects that take up a computer's memory to store. Technically,only the data is dynamic, but the function will remain the same no matter how many times we create an object. Can we do it better?

What if I told you that we could save up some memories by putting all functions on the upper chain and sharing them all with the newly created object?

The power of Object.create

Object.create is used for creating objects with a specified prototype. This allows you to create objects that inherit properties and methods from another object (the prototype).

const functionStore = {
   increment:function() { this.score++; },
   login:function() { console.log("you log in"); }
}

function userCreation(name,score) {
  const newUser = Object.create(functionStore);
  newUser.name = name;
  newUser.score = score;
  return newUser;
}

💡
[[Prototype]] is a special property that links to prototype property that contains a bunch of re-usable functions

Let's have a look at this keyword again.

// [[Prototype]]:Object
const functionStore = {
   increment:function() { this.score++; },
   login:function() { console.log("you log in"); }
}

This keyword is a very flexible word in JS that depends on the object calling a function, In our case, we group our store function object in higher up the chain and we might not know what the value of the object that executes our function is, but with this, it replies on the object calling our function

newUser.increment;
// It will increase property score of the newUser to 1;

This,call,apply and bind with fns in JS

You might have noticed that putting the share common function in the upper chain is a great way to cut down on the computer's memory resource, but you should also think about how it knows the context of the value is calling to the function. Obviously, there could be a thousand of objects sharing the same function together so this keyword is the way JS offers us to solve this issue

Understand the context and this

const myTruck = {
    speed:50
}
function drive() {
   console.log(`Driving at ${this.speed}`);
}

Binding the current implementation of a function to the current context invokes function

//Bind first and execute later
const boundDrive = drive.bind(myTruck);
boundDrive();
// Driving at 50
drive.call(myTruck);
// Driving at 50;
drive.apply(myTruck);
// Driving at 50

Difference between bind,call and apply

  • bind creates a new function with a specified "this" value

  • call invokes a function immediately with a specified "this" value and individual arguments

  • apply invokes a function immediately with "this" value and an array of arguments

💡
When we write the function using this keyword,. We imply that in the feature that someone can borrow this function from us :)
const person = {
  name: "John",
  sayHello: function(greeting) {
    console.log(`${greeting}, ${this.name}!`);
  }
};

const anotherPerson = {
  name: "Jane"
};

// Using bind
const greetJohn = person.sayHello.bind(person, "Hello");
greetJohn(); // Outputs: Hello, John!

// Using call
person.sayHello.call(anotherPerson, "Hi"); // Outputs: Hi, Jane!

// Using apply
person.sayHello.apply(anotherPerson, ["Hola"]); // Outputs: Hola, Jane!

Borrow Array.prototype.map to use with string

Array.prototype.map = function(callback, thisArg) {
    const newArray = [];
    //Whoever calls me must have a length property
    for (let i = 0; i < this.length; i++) {
        if (this.hasOwnProperty(i)) {
            newArray[i] = callback.call(thisArg, this[i], i, this);
        }
    }

    return newArray;
};

// Example to use myMap with a string
function splitter(string) {
  return Array.prototype.map.call(string, (x) => x);
}

console.log(splitter('Hello World'));
💡
Most of the utilities we found in JS (array, object, JSON , etc) will respond heavily to the "this" mechanism

Why can we call the bind,call, apply on function?

It turns out that everything in JS is an object. Keep in mind that when we create a function in JS, it has a special power that lets us do something significant.

💡
Is it weird that we can invoke a property on a function in Javascript?

Prototype in JS

We can simplify the Object.create function with prototype in JS and it will work the same

function UserCreator(name,score) {
    this.name = name;
    this.score = score;
}
// we create an empty object and assign it value with name and score


UserCreator.prototype.increment = function () {
    this.score++;
}

UserCreator.prototype.login = function () {
    console.log("login");
}

const user1 = new UserCreator("Vince",100);
💡
In JS , we can execute a function by doing (); and we can even create an object with function with the new keyword

Note that the prototype property also exists in an array. Think back every time we use array methods like push(), join(), etc. All these built-in array-related functions are actually stored inside an array at Array.prototype

The new keyword

If you notice that we don't have to manually create an object and return an object. The new keyword when calling a function will automatically create an object and return an object for us

The class 'syntatic sugar'

With that solution using the prototype keyword, it's very implicit, but it exposes so many low-level details to the users. We want some magic to somehow replicate the traditional language like Java and C++ do. So welcome to the syntatic sugar class keyword of javascript

class UserCreator {
    constructor(name,score) {
        this.name = name;
        this.score = score;
    }
    increment() {
        this.score++;
    }
    login() {
        console.log("login");
    }
}
💡
This is still working the same behind the hood but is more elegant visually

The static keyword

In the previous examples, you saw how to define object methods (prototype methods), accessible to all object instances. In addition to such methods, classical object-oriented languages such as Java and C# use static methods, which are defined at the class level. Check out the following example:

💡
Static functions doesn't based on context
class Ninja{
  constructor(name, level){
    this.name = name;
    this.level = level;
  }

  swingSword() {
    return true;
  }

  static compare(ninja1, ninja2){
    return ninja1.level - ninja2.level;
  }
}
// How to use it
Ninja.compare(new Ninja("Vince",100), new Ninja("MA",1000) );
// Static method is used at the class level

Behind the scene, it's also another syntatic sugar for class-based behaviors

function Ninja(name,level) { };
Ninja.compare = (ninja1,ninja2)=> { }

There're a lot of example out there using static methods (utils for one-time use)

  • Object.keys, Object.values, Object.freeze, Object.defineProperty,etc

  • Array.isArray,Array.from

Inheritance in JS

class Person {
   constructor(name) {
      this.name=name;
   }
   dance() {
    return true;
   }
}

class Ninja extends Person {
   constructor(name,weapon) {
       super(name); // Person.call(this,name);
       this.weapon=weapon;
    }
    wieldWeapon() {
       return true;
    }
}

const ninja = new Ninja("Bob","knife");

//Let me prove to you classes is just a prototype in JS
Person.prototype.isPrototypeOf(ninja);

Behind the scene

// Equivalent ES5 code
function Person(name) {
   this.name = name;
}

Person.prototype.dance = function () {
   return true;
};

//Assign properties
function Ninja(name, weapon) {
   Person.call(this, name);
   this.weapon = weapon;
}

//Link Ninja prototype to Person prototype, otherwise it doesn't work
Ninja.prototype = Object.create(Person.prototype);


Ninja.prototype.constructor = Ninja;
Ninja.prototype.wieldWeapon = function () {
   return true;
};

Encapsulation

class Mammal {
    constructor(sound) {
      this._sound=sound;
   }
   talk() {
     return this._sound;
   }
}

class Dog extends Malmal {
    constructor(sound) {
        super(sound);
    }
}

const funky = new Dog("WOOO");
funky._sound; // we can still access this
💡
As we already know, JS doesn't have class. JS fakes classes so there's no such things as a private field in JS :(

TS stories

Declare a variable without constructor

class Vehicle {
    color:string="red";
}

// equivelant to 
class Vehicle {
    constructor() {
        this.color = "red";
    }
}

Typescript adds public, protected and private field modifiers that seem to provide some enforcement

class Diary {
  private secret = 'cheated on my English test';
}

const diary = new Diary();
diary.secret;
// Property secret is private and only accessible within class 'Diary'
💡
Note to take here: private is a feature of TS and it will go away at run time

Don't reply on private to hide information since it's only there to prevent users from accessing it but users can choose not to obey the rules

(diary as any).secret;

Short-way for declaring variable and initialize it

And assign modifier to the variable inside class

public Piece {
    constructor(private readonly color:Color,
                public autoAssign:string,
                private file:File,
                private rank:Rank) {
        this.position = new Position(file,rank);
     }
}

Auto assign the parameter to this

public Piece {
    constructor(color,autoAssign,file,rank) {
        this.position = new Position(file,rank);
        this.color = color;
        this.file = file;
        this.rank = rank;
        this.autoAssign = autoAssign
     }
}
💡
new Piece takes three parameters: color, file, and rank. We added three modi‐ fiers to color: private, meaning assign it to this and make sure it’s only accessi‐ ble from an instance of Piece, and readonly, meaning that after this initial assignment it can only be read and can’t be written anymore.

Abstract class

We've defined a class Piece but we don't want users to instantiate a new Piece but rather extends from it

abstract class Animal {
    // Abstract method (does not have an implementation)
    abstract makeSound(): void;

    // Regular method
    move(): void {
        console.log("Moving along...");
    }
}

class Dog extends Animal {
    // Providing the implementation for the abstract method
    makeSound(): void {
        console.log("Woof! Woof!");
    }
}

class Cat extends Animal {
    // Providing the implementation for the abstract method
    makeSound(): void {
        console.log("Meow! Meow!");
    }
}

super

  • method calls, like super .take to override parent's method implementation

Constructor calls, which have the super to call constructor function of parent's constructor

💡
Note that we can only access parent class's methods, and not its properties, with super

Builder pattern

Builder pattern is a way to seperate the construction of an object from the way that object is actually implemented (JQuery, ES6 data structure Map and Set, the style of API looks really the same)

new RequestBuilder()
    .setURL("/users");
    .setMethod("get");
    .setData({firstName:"Anna"})
    .send();

// We can guess that it has some default variables
// and we can set via it publics methods
class RequestBuilder() {
    constructor() {
       this.data = null;
       this.method = "get";
       this.url = null;
     }
}

Implementation

class RequestBuilder {
    private data: object | null = null;
    private method: "get" | "post" | null = null;
    private url: string | null = null;

    setMethod(method:"get" | "post"):this {
       this.method = method;
       return this;
    }
    setData(data:object):this {
       this.data = data;
       return this;
     }
    setURL(url:string):this {
      this.url = url;
      return this;
    }
}

Building sorter class

First attempt

class Sorter {
    constructor(public collection:number[]) {}
    sort():void{
      const {length} = this.collection;
      for(let i=0; i<length;i++) {
        for(let j=0; j<length-i-1;j++) {
            if(this.collection[j] > this.collection[j+1])  {
                const leftHand = this.collection[j];
                this.collection[j] = this.collection[j+1];
                this.collection[j+1] = leftHand;
            }
        }      
      }
}
💡
First problem is string are immutable if we decide to support sort a string this will not work
💡
Second problem with compare string is "A".codePointAt() [65] while "a".codePointAt() [97] (ASCII) so the comparasion is the large number is first then smaller number (in-term-of string comparasion). It's in opposite way of doing number comparasion

Second attempt

With union operator

class Sorter {
    constructor(public collection:number[] | string) {}
    sort():void{
      const {length} = this.collection;
      if(Array.isArray(this.collection){
      for(let i=0; i<length;i++) {
        for(let j=0; j<length-i-1;j++) {
            if(this.collection[j] > this.collection[j+1])  {
                const leftHand = this.collection[j];
                this.collection[j] = this.collection[j+1];
                this.collection[j+1] = leftHand;
            }
            }      
         }
      }
      if(typeof this.collection === "string") {...}
}
💡
If we would want more supports of more data structure, then it will become many if-else to handle more types (TreeStructure, Linked-list and more types)

Third-attempt

With inversion of control

class NumbersCollection {
    constructor(public data:number[]) {}
    get length():number {
        return this.data.length;
    }    
    compare(leftIdx:number,rightIdx:number):boolean {
        return this.data[leftIdx] > this.data[rightIdx];
    }
    swap(leftIdx:number,rightIdx:number):void {
        const leftHand = this.data[lefxIdx];
        this.data[leftIdx] = this.data[rightIdx];
        this.data[rightIdx] = leftHand;
    }
}

class Sorter {
    constructor(public collection: NumbersCollection) {}
    sort():void {
      for(let i=0; i<length;i++) {
        for(let j=0; j<length-i-1;j++) {
            if(this.collection.compare(j,j+1)){
                this.collection.swap(j,j+1);
            }
         }
       }
}

Forth attempt

Giving instructions to other classes on how to do compare and sort(interface)

interface Sortable {
    length:number;
    compare(leftIdx:number,rightIdx:number):boolean;    
    sort(leftIdx:number,rightIdx:number):void;
}

Fifth attempt

Building sorting for string

class CharacterCollection extends Sortable {
    constructor(public data:string) { }
    get length() {
        return this.data.length;
    }
    compare(leftIdx:number,rightIdx:number) {
        return this.data[leftIdx].toLowerCase() > this.data[rightIdx].toLowerCase();
    }
    swap(leftIdx:number,rightIdx:number) {
        const characters = this.data.split(/\s+/);
        const leftHand = this.characters[leftIdx];
        this.characters[leftIdx] = this.characters[rightIdx];
        this.characters[rightIdx] = leftHand;
        this.data = this.characters.join(" ");
    }
}

Sixth attempt

The solution above is fine but it's too verbose

const numberCollections = new NumbersCollection(1,2,3);
const sortNumber = new Sort(numberCollections);
sortNumber.sort();

Passing down the sort methods to all of the collection class would be a better choice

interface Sortable { }

class Sorter {
     sort():void {
      for(let i=0; i<length;i++) {
        for(let j=0; j<length-i-1;j++) {
     // Type error => there's no definition of this in parent class
     // You can't reference to the methods of its children in parent  
            if(this.compare(j,j+1)){
                this.swap(j,j+1);
            }
         }
       }
}

class NumberCollections extends Sorter{
    constructor(numbers:number[]) {
        super();
    }
}

Abstract class can handle this situation

  • Can't be used to create an object directly

  • Only used as a parent class

  • The implementation methods can refer to other methods don't actually exist yet

  • Can contain real implementation for some methods

  • Can make children classes promise to implement some other methods

abstract class Sorter {
    // This will be implemented in the future
    abstract compare(leftIdx:number,rightIdx:number):boolean;
    abstract swap(leftIdx:number,rightIdx:number):void;
    abstract length:number;

     sort():void {
      for(let i=0; i<length;i++) {
        for(let j=0; j<length-i-1;j++) { 
            if(this.compare(j,j+1)){
                this.swap(j,j+1);
            }
         }
       }
}
}

class NumbersCollection extends Sorter {};
class CharactersCollection extends Sorter {};

Use-case

Implements streaming from node

var stream = require('stream');
function StatStream(limit) {
    stream.Readable.call(this);
    this.limit = limit;
}

StatStream.prototype = Object.create(stream.Readable.prototype,{
    constructor: {value:StatStream};
})

StatStream.prototype._read = function (size) {
    // Must implement it (abstract methods)
}

Interfaces vs Abstract classes

Inheritance in real-life

At this time, we already know the value of inheritance system, so let's inherit some common methods that we don't have to write from scratch

💡
And all of this happened because of the this keyword ;)
  • Array methods

  • Object methods

  • EventTarget Interface

  • Event interface

  • And so on. Even we can build our own class with our business logic and let others inherit from it

💡
Inheritance is nice, but it does come with a price. For example, in the case of an object vs. a map data structure in Javascript,. If we want to create a truly dictionary object without any irrelevant methods, I suggest using a map data structure instead

DOM with the inhertance concept?

Keep in mind that these are the properties values

Its prototype

Event with inheritance concept?

Its prototype

Inheritance vs composition

//Instead of building from prototype chain , we seperate our 
//common functions
const barker = (state)=> ({
    //Return a new object with bark method
    bark:()=> console.log("Woof, I'm" + state.name);
})

const driver = (state)=> ({
    //Return a new object with drive method
    drive:()=> state.position = state.position + state.speed
})

//Instead of forcing users to inherit these fns we give them
//the ability to compose it

const muderRobotDog = ()=>{
    let state = {
        name,
        speed,
        position:0
     };
    return Object.assign({} , barker(state) , driver(state) );
}

const murderRobot = murderRobotDog();
murderRobot.bark();
murderRobot.drive();
💡
Take a moment to think about the advantage of a prototype versus allowing users to freely combine our common functions.