Prototype and Prototypal Inheritance

Everything in JS is a object.

When you create a array, object, variable or function. It will be chained as object. To that object we have access to so many methods.

const arr = [1, 2, 3, 4];
const name = "Arjunan";
const obj = {
  age: 12,
  place: "London",
};
function foo() {
  console.log("Foo");
}

// arr.length
//    .map
//    .reduce

// obj.toString()
//    .toLocaleString()
//    .valueoOf

// name.at
//     .concat
//     .indexOf

// foo.length
//    .call
//    .apply
//    .bind

1. What is __proto__?

__proto__ is a object in JS, that refers to the prototype of an Object.

It is considered outdated, and you should use Object.getPrototypeOf() instead.

The reason for this type of naming convention is to the prevent use of prototype by mistake.

const obj = {};
const arr = [];
const abc = 10;
const xyz = 10;

const protoOfObj = Object.getPrototypeOf(obj);
console.log(obj.__proto__); // {constructor: ƒ, __defineGetter__: ƒ, ...}
console.log(protoOfObj === obj.__proto__); // true
console.log(Object.prototype === obj.__proto__); // true
console.log(obj.prototype); // undefined

2. What is Prototype Chaining?

At the end whatever we create, be it a variable, array, object or function. If we check the __proto__ in a chained manner. All ends up being an object.

const arr = [];
const abc = 10;
const xyz = "Arjun";
const foo = () => {
  console.log("foo");
};

console.log(arr.__proto__); // [constructor: ƒ, at: ƒ, concat: ƒ,...]
console.log(Array.prototype); // [constructor: ƒ, at: ƒ, concat: ƒ,...]
console.log(arr.__proto__.__proto__); // {constructor: ƒ, __defineGetter__: ƒ,...}
console.log(Object.prototype); // {constructor: ƒ, __defineGetter__: ƒ,...}
console.log(arr.__proto__.__proto__.__proto__); // null

console.log(abc.__proto__); // Number {0, constructor: ƒ, toExponential: ƒ,...}
console.log(Number.prototype); // Number {0, constructor: ƒ, toExponential: ƒ,...}
console.log(abc.__proto__.__proto__); // {constructor: ƒ, __defineGetter__: ƒ,...}

console.log(xyz.__proto__); // String {'', constructor: ƒ, anchor: ƒ, at: ƒ,...}
console.log(String.prototype); // String {'', constructor: ƒ, anchor: ƒ, at: ƒ,...}
console.log(xyz.__proto__.__proto__); // {constructor: ƒ, __defineGetter__: ƒ,...}

console.log(foo.__proto__); // ƒ () { [native code] }
console.log(Function.prototype); // ƒ () { [native code] }
console.log(foo.__proto__.__proto__); // {constructor: ƒ, __defineGetter__: ƒ,...}

When you try to access the methods or properties on the object, the object checks if that instance is available on it or not, if it is not available it will look for the same in the prototype. This is called a prototype chain where the derived object keeps looking for the properties in its prototype, this really helps in creating a single instance of the property on the constructor making it available to all the derived instances of it.

The new printDetails method added via prototype is accessible to all instance of Robot.

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

Robot.prototype.printDetails = function () {
  console.log(this.name, this.age);
};
Robot.prototype.name = "ABC"

const robot1 = new Robot("Robocop", 10);
const robot2 = new Robot("Ironman", 12);
robot1.printDetails(); // Robocop 10
robot2.printDetails(); // Ironman 12

Even if we have added the property name to the prototype, the methods accessing the property value from its nearest scope. If we remove the name property from the constructor then it will access it from the prototype chain.

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

Robot.prototype.printDetails = function () {
  console.log(this.name, this.age);
};
Robot.prototype.name = "ABC";

const robot1 = new Robot("Robocop", 10);
const robot2 = new Robot("Ironman", 12);
robot1.printDetails(); // ABC 10
robot2.printDetails(); // ABC 10

3. What is Prototypal Inheritance?

In the below example, Object2 is inheriting the properties of Object1. So when some methods or arguments are called in Object2, first it will check inside Object2. If not found, it will inherit and check in Object1. This is called Prototypal Inheritance.

const object1 = {
  name: "Arjunan",
  age: 12,
  getIntro() {
    console.log(this.name + " is " + this.age + " years old.");
  },
};

const object2 = {
  name: "John",
};

console.log(object2.__proto__); // {constructor: ƒ, __defineGetter__: ƒ,...}
object2.__proto__ = object1; // Never do this (creates performance issues)
console.log(object2.__proto__); // { name: 'Arjunan', age: 12, getIntro: [Function: getIntro] }
object2.getIntro(); // John is 12 years old.
class Robots {
  constructor(name) {
    this.name = name;
  }

  getName() {
    console.log(this.name);
  }
}

class Robot extends Robots {
  constructor(name, year) {
    super(name);
    this.year = year;
  }

  getYear() {
    console.log(this.year);
  }
}

class User extends Robot {
  constructor(name, year, type) {
    super(name);
    this.year = year;
    this.type = type;
  }

  getType() {
    console.log(this.type);
  }
}

const user = new User("Ironman", 2000, "Avenger");
user.getType(); // Avenger
user.getYear(); // 2000
user.getName(); // Ironman

4. Why we create functions directly to prototype?

So that it will be accessible to every Array, Object and Functions we create.

Array.prototype.myMap = function () {};
Function.prototype.myBind = function () {};
Object.prototype.myGroupBy = function () {};