DEV Community

Cover image for Deep Dive into OOP, Prototypes, and the 'new' Keyword in JavaScript
Asim Momin
Asim Momin

Posted on • Originally published at asim-momin-7864.hashnode.dev

Deep Dive into OOP, Prototypes, and the 'new' Keyword in JavaScript

Context & About Me

I am a B.Tech IT student currently re-learning JavaScript internals deeply to master Backend Engineering.

I learned these concepts from Hitesh Choudhary (channel name: Chai aur Code). I watch his videos, understand them, and write notes on what he explains. This blog is a compilation of those notes, where I converted his Hindi explanations into my own English understanding to make these concepts simple.


1. Object Literals and the this Keyword

Object Literal: Creating an Object. The Object is the base unit of the JS language.

Object Example


const user = {
  username: "Hamada",
  loginCount: 24,
  isLoggedIn: true,
  greeting: function () {
    // 'this' keyword represents context. 
    // It answers: "We are talking about which user object?"
    // If there are other similar objects, then what?
    console.log(`hello! ${this.username} `); 
    console.log(" this  --> ", this); // this represents the current user object
  },
};

// Accessing properties
console.log("username --> ", user.username);

// Accessing methods

// Reference
console.log("method reference --> ", user.greeting);

// Executing
console.log("method executing --> ", user.greeting());


Enter fullscreen mode Exit fullscreen mode

2. Why do we need a Constructor?

Reason 1: If I need to make another user object having similar fields, then I need to copy-paste the code again.


let user2 = {
  username: "king",
  loginCount: 2,
  isLoggedIn: false,
  greeting: function () {
    console.log(`hello! ${this.username} `);
    console.log(" this  --> ", this);
  },
};

Enter fullscreen mode Exit fullscreen mode

Reason 2: Why do we create new multiple instances from one Object / Class using the "new" keyword? Why can't we use a single instance?

Let's understand this with a function example.


function displayUsers(username, age, email) {

  // myUsername = username; but we mostly keep the same names.
  // username = username; if we want the same name for var in function as argument to store, this way causes some issues.

  this.username = username; // so we use "this" to tell username in this function

  this.age = age;
  this.email = email;

  return this; 

  // Also, if we are using the "new" keyword and creating instances for this function, 
  // we don't need to write "return this". The constructor automatically does this.
  // But explicitly writing it is good practice.
}

Enter fullscreen mode Exit fullscreen mode

EXPLANATION: The "return this" behavior

  • In a normal function, we write return this to get the object back.

  • BUT, when we use the new keyword, JavaScript is smart! The new keyword AUTOMATICALLY (implicitly) returns this (the new object) for us.

  • So, even if you don't write return this, it will still work perfectly with new. Writing it explicitly is just like a "safety backup" or good practice, but not strictly needed.

SUPER IMP POINT FOR INTERVIEWS:

If your function returns a "Primitive" (like a string, number, or true/false), the new keyword IGNORES it and still returns your object.

BUT, if your function returns a "Different Object" (like {name: "luffy"}), then new will throw away your intended object and return that different object instead!


3. The new Keyword and Instances

Let's see why we need instances.


// userOne created
const userOne = displayUsers("Hamada", 12, "hamada@g.com");
const userTwo = displayUsers("King", 24, "king@kong.in");

console.log(" userOne --> ", userOne); 

// --> Working correctly when we use that function once only meaning we create only userOne.

username: 'Hamada'
age: 12...

Enter fullscreen mode Exit fullscreen mode

// userOne created
const userOne = displayUsers("Hamada", 12, "hamada@g.com");

// userTwo created
const userTwo = displayUsers("King", 24, "king@kong.in");

console.log(" userOne --> ", userOne); 

/*

userOne is giving output -->

username: 'King'
age: 24...

*/

Enter fullscreen mode Exit fullscreen mode

When we create userTwo with the same function instance, this time it won't work.

That's why we need to create different instances to avoid overwriting values.


const userThree = new displayUsers("Luffy", 18, "luffy@op.com");
const userFour = new displayUsers("Gojo", 28, "gojo@op.com");

console.log("userThree --> ", userThree); 

// userThree -->  displayUsers { username: 'Luffy', age: 18, email: 'luffy@op.com' }

// Worked perfectly

Enter fullscreen mode Exit fullscreen mode

Few points about how the "new" keyword works

  1. First, it creates a new empty object.
  2. The new keyword calls a constructor function.
  3. The constructor function packs all arguments and definitions and returns them to you.
  4. Then all these arguments are injected into the this keyword.
  5. You get all these arguments inside this.

Constructor Property


console.log(userThree.constructor); // --> [Function: displayUsers]

// The constructor property is a reference to itself, meaning a reference to the displayUser function.

Enter fullscreen mode Exit fullscreen mode

instanceof Operator

One operator to check if an instance is created from the original/wanted object or class.


console.log(
  "is userThree is instance of displayUser func --> ",
  userThree instanceof displayUsers,
); // --> true

Enter fullscreen mode Exit fullscreen mode

4. What is a Prototype?

  • Prototype is the default behavior / core working mechanism of JavaScript. We also call it "prototypic behavior".

  • In JS, everything is an Object, or we can say everything is made from an Object.

Diagram: The Prototype Chain

The Prototype Chain Dig

  • Every element/component has its own prototype (default properties and methods).

  • Inside each element's prototype, there is the prototype of its parents as well. Then again, inside that parent's prototype, its parent exists (meaning the first element's grandparent prototype exists)... and it continues until we reach the Object's prototype. That is the end; after that, we get null.

  • Object does not have any parent.

One interesting nature of JS

If it does not find any method or property in that element's prototype, then it goes deeper into its parent's prototype and tries to find that property or method. If not found, it goes further into the depths of its grandparent's prototype.

prototype -----> prototype ----->(Object) prototype -----> Null

Due to this nature, and because everything is made from Object, other elements like Function and Array behave like Objects too.

Example: Function acting like an Object


// create one function
function additionTwo(num) {
  return num + 2;
}

// I try to create a key-value pair like an Object into this function
additionTwo.username = "Luffy";

// working - normally
console.log(additionTwo(5)); // -> 7

// accessing its key-value pair
console.log(additionTwo.username); // -> Luffy

Enter fullscreen mode Exit fullscreen mode

How is this possible?
This is possible due to the interesting JS nature explained above.


5. How .prototype looks


console.log(additionTwo.prototype); // -> { }

Enter fullscreen mode Exit fullscreen mode

It looks like it is an empty object, but it is not empty. It is not made to be visible; it is for the internal core to use and access.

The "this" keyword and its working in Prototype

  • The this keyword is also connected to the prototype.

  • It helps to set/create context (context means: who is calling? who is creating? who is accessing? like which element is it? on which elements do we apply operations/methods?).

someElement.prototype --> { }

One IMP Line ~ very deep line:

"That { } prototype means there are some default set methods and some internal properties, so their/that method's and property's this (context) is in that prototype { }."
"prototype { } = other all set hidden properties + this (context) of that method."

Let's see one example on this and how it sets context.


function dishAndPrice(name, price) {
  this.name = name;
  this.price = price;
  console.log(` Dish added into menu `);
}

Enter fullscreen mode Exit fullscreen mode

Now let's create our own methods for the function.

Without "this"


dishAndPrice.prototype.tellMenu() = function() {
    console.log(`Menu : Dish name is ${name} and price is ${price}`);
};

let Pizza = new dishAndPrice("Veg Jumbo Pizza", 250 );
Pizza.tellMenu();

// output --> 
// Throw Error --> ReferenceError: name is not defined

Enter fullscreen mode Exit fullscreen mode

As we know, both instances share the same single prototype. All these methods and properties are kept in the prototype, so when any instance calls the tellMenu method, it gets confused: "Which one is calling me?" and "Which name and price should I print?"

To avoid this kind of confusion, we use this.

With "this"


dishAndPrice.prototype.tellMenu = function () {
  console.log(`Menu : Dish name is ${this.name} and price is ${this.price}`);
};

let pizza = new dishAndPrice("Veg Jumbo Pizza", 250);
let noodles = new dishAndPrice("Hakka Chilly Noodles", 80);

pizza.tellMenu();
noodles.tellMenu();

// output --> 
// Menu : Dish name is Veg Jumbo Pizza...

Enter fullscreen mode Exit fullscreen mode
  • Now, tellMenu knows whose name and price to print when it is called.

  • Here we tell it: "Print the name and price of whichever instance is calling you."

  • This is the context we are talking about, and the this keyword helps to create it.


6. Theory behind the working of the "new" keyword

It is deep and hard to understand. Here is what happens behind the scenes when the new keyword is used:

  1. A new object is created: The new keyword initiates the creation of a new JavaScript object.

  2. A prototype is linked: The newly created object gets linked to the prototype property of the constructor function. This means that it has access to properties and methods defined on the constructor's prototype.

  3. The constructor is called: The constructor function is called with the specified arguments, and this is bound to the newly created object. If no explicit return value is specified from the constructor, JavaScript assumes this (the newly created object) to be the intended return value.

  4. The new object is returned: After the constructor function has been called, if it doesn't return a non-primitive value (object, array, function, etc.), the newly created object is returned.


7. Prototypal Inheritance

Goal: Our goal is that our own custom-created method should be available for all elements of that kind.
E.g., if I create a hello() method for Array, then all Arrays should have this method automatically.

Example: String


let myName = "Luffy";

console.log(myName.length); // -> 11

// The length property is counting white spaces also.

// So we want a property that gives us the actual length (remove spaces and then count).

// Give it the name "trueLength".

Enter fullscreen mode Exit fullscreen mode

And this method should be available in every String. Not only for the parent/original element but also its instances.

Flow of inheritance (From which direction are properties passed?)

IMP Diagram (Flow of Inheritance)

Flow of Inheritance

Only Down Flow (Parent ---> Child)

No Up Flow (Child ---> Parent)

Down Flow (Parent ---> Child)

I want one Method in all (Array, Function, Object, etc.). As we know everything is an Object, if we inject our custom method into Object, then it will be available in all due to the inheritance flow.


// Creating and injecting our custom Method
Object.prototype.greeting = function () {
  console.log("Hello! Nice to meet you");
};

let myHeros = ["Superman", "Batman", "Iron Man"]; // array
let animal = { name: "parrot", ability: "fly" }; // object

// array --> it inherits
myHeros.greeting(); // --> Hello! Nice to meet you

// object --> it inherits
animal.greeting(); // --> Hello! Nice to meet you

Enter fullscreen mode Exit fullscreen mode

--> Yes, properties and methods flow of inheritance is from Parent ----> Child.

Up Flow (Child ---> Parent)

Let's understand: does our flow of inheritance flow backward?


// creating custom method for Array
Array.prototype.goodNight = function () {
  console.log("Good Night");
};

//* array --> it inherits


myHeros.goodNight(); //* --> Good Night

// object (parent) -->  //!  it does not inherit
// animal.goodNight(); //! --> animal.goodNight is not a function

Enter fullscreen mode Exit fullscreen mode

--> No, properties does not flow Up (Child —> Parent )

Answer for our earlier task: creating trueLength for String


String.prototype.trueLength = function () {
  console.log(this.trim().length);
};

// using our custom method
myName.trueLength(); // --> 5

Enter fullscreen mode Exit fullscreen mode

8. Modern Syntax vs Old Syntax for Inheritance

#1 __proto__ (Older Syntax)

__proto__ is consider as a property.


const livingThing = { isGrowing: true };
const plant = { name: "Apple Tree" };
const fruit = {
  haveSeeds: true,
  // Syntax 1 (inside)
  __proto__: plant,
};

// Syntax 2 (outside)
plant.__proto__ = livingThing;

// Let's check: did our inheritance work?
console.log(fruit.isGrowing); // --> true

Enter fullscreen mode Exit fullscreen mode

#2 Object.setPrototypeOf (Modern Syntax)


const vehical = { haveFourWheels: true };
const car = { name : "Mercedes Benz S-Class" };

// Modern Syntax
Object.setPrototypeOf( car, vehical );

Enter fullscreen mode Exit fullscreen mode

Credits & Source:

Top comments (3)

Collapse
 
jonrandy profile image
Jon Randy šŸŽ–ļø

... and it continues until we reach the Object's prototype.

Careful... not every object has Object.prototype in its prototype chain.

Collapse
 
asim-momin-7864 profile image
Asim Momin • Edited

Thanks for pointing this out!! I’m trying to learn JS deeply to have better in-depth knowledge, but I haven't come across this specific case or read about it yet.

I searched about it ... I found that you are talking about creating a pure object that doesn't inherit anything not even the object's common methods

Thanks for sharing this.

Collapse
 
jonrandy profile image
Jon Randy šŸŽ–ļø • Edited

Yes... it's trivial, and sometimes useful, to create such objects:

Object.create(null)
Enter fullscreen mode Exit fullscreen mode

Some built-in functions also return objects with null-prototypes: Object.groupBy being an example.