JavaScript Objects and Prototypes In-depth

Comprehensive notes on the JavaScript Objects and Prototypes In-depth series by Java Brains. This resource dives deep into JavaScript's core concepts, including objects, prototypes, inheritance, and the prototype chain, with clear explanations and practical examples. Perfect for mastering the foundations of JavaScript object-oriented programming!

📺 Watch on YouTube

Unit 01 - Creating Objects

01 - Introduction

This course explores JavaScript’s object-oriented programming, covering object creation, constructors, execution contexts, prototypes, and practical coding examples.

Highlights

Note: The ES6 version has a class keyword that gives class like syntax but we still don’t get all the typical class features.

02 - Objects Basics

JavaScript objects are collections of values, allowing for flexible structures and properties. They can be created in various ways, including inline.

Highlights

03 - Creating Objects

Over here we learnt how to create employee objects with properties such as firstName, lastName, gender, and designation.

const emp1 = {
  firstName: "John",
  lastName: "Doe",
  gender: "Male",
  designation: "Software Engineer",
};
const emp2 = {
  firstName: "Jane",
  lastName: "Smith",
  gender: "Female",
  designation: "Project Manager",
};

However, manually creating multiple employee objects could quickly become repetitive and inefficient.

To address this, let’s introduce a reusable function called createEmployeeObject, which accepts parameters for each property and dynamically generates employee objects.

function createEmployeeObject(firstName, lastName, gender, designation) {
  var newObj = {};
  newObj.firstName = firstName;
  newObj.lastName = lastName;
  newObj.gender = gender;
  newObj.designation = designation;

  return newObj;
}

// Using the function to create employee objects
const emp1 = createEmployeeObject("John", "Doe", "Male", "Software Engineer");
const emp2 = createEmployeeObject("Jane", "Smith", "Female", "Project Manager");
const emp3 = createEmployeeObject("Emily", "Taylor", "Female", "QA Tester");

This approach avoids the redundancy of manually defining each object, as shown in the earlier examples of emp1 and emp2, and allows to efficiently create additional employee objects like emp3. By leveraging this function, one can streamline object creation, reduce errors, and improve code maintainability in scenarios involving multiple similar objects.

04 - JavaScript Constructors

Constructor functions in JavaScript simplify object creation by eliminating repetitive code, allowing developers to use the new keyword for efficient object initialization.

In the provided code below,

function createEmployeeObject(firstName, lastName, gender, designation) {
  var newObj = {};
  newObj.firstName = firstName;
  newObj.lastName = lastName;
  newObj.gender = gender;
  newObj.designation = designation;

  return newObj;
}

The process of creating and returning a new object (var newObj = {} and return newObj) is repetitive when writing multiple functions to create different types of objects. JavaScript simplifies this using a constructor function, which is called with the new keyword. Unlike other languages where the new keyword is used with a class name, in JavaScript, it is used with a function. The new keyword automates the creation of a new object and assigns it to this, making it available within the function. Behind the scenes, JavaScript essentially does the following:

function createEmployeeObject(firstName, lastName, gender, designation) {
  // var this = {};
  this.firstName = firstName;
  this.lastName = lastName;
  this.gender = gender;
  this.designation = designation;
  // return this;
}

The commented lines (var this = {} and return this) represent the implicit actions performed by JavaScript when a constructor function is invoked using new. This eliminates the need to explicitly create and return the object, streamlining object creation in the codebase.

Highlights

05 - Difference between regular functions and constructors

It explores two methods for creating objects in JavaScript: a standard function and a constructor function with the new keyword. The first method, createBicycle, uses a standard function to create objects manually. It initializes an empty object, assigns properties like cadence, speed, and gear from the provided arguments, and then returns the constructed object.

function createBicycle(cadence, speed, gear) {
  var newBicycle = {};
  newBicycle.cadence = cadence;
  newBicycle.speed = speed;
  newBicycle.gear = gear;
  return newBicycle;
}

var bicycle1 = createBicycle(50, 20, 4);
var bicycle2 = createBicycle(20, 5, 1);

We also have, bicycleConstructor, which simplifies object creation. By using the new keyword, JavaScript automatically initializes a new object, binds it to this, and returns it implicitly. The function defines properties directly on this, avoiding the need for explicit initialization and return statements.

function bicycleConstructor(cadence, speed, gear) {
  this.cadence = cadence;
  this.speed = speed;
  this.gear = gear;
}

var bicycle3 = new bicycleConstructor(50, 20, 4);

Although JavaScript lacks explicit markers for constructor functions, the naming convention of starting function names with a capital letter i.e. PascalCase (e.g., BicycleConstructor) serves as a visual cue for their intended use. Additionally, the segment compares JavaScript's constructors to the class-based syntax in other languages and hints at potential pitfalls when constructors are used improperly, setting the stage for future discussions.

💡 Note: If you try to call a constructor function without new keyword, it will not work.

06 - Switching function types and calls

It explains the differences between using constructor functions with the new keyword versus calling them as regular functions, emphasizing the importance of proper usage.

Constructor functions in JavaScript are specifically designed to be called with the new keyword, which automates the creation of a new object, binds it to this, and implicitly returns it. In contrast, regular functions can create objects without the need for new. However, using the new keyword with a regular function can lead to unnecessary code execution and inefficiencies.

When a constructor function is called without the new keyword, JavaScript does not automatically create or return an object, which often results in the function returning undefined. This happens because JavaScript defaults to returning undefined when no explicit return statement is provided. On execution of constructor function without new keyword, this keyword will refer to global window and the properties will be assigned to global window object. This behavior underscores the importance of correctly distinguishing between regular functions and constructor functions.

Mixing these function types without adhering to conventions, such as capitalizing constructor function names, can lead to unexpected behaviors, errors, and confusion in the codebase. Following established naming conventions and usage patterns helps ensure clarity, proper function usage, and maintainable code.


Unit 02 - Understanding the this reference

07 - Function Execution Types

Understanding function execution types in JavaScript is essential for grasping the this keyword. JavaScript functions can be executed in four distinct ways, which influence how the this context is determined. Understanding these methods is crucial for advanced JavaScript development.

• 🧠 Direct Function Call: Calling a function directly executes it in the global context or the local context if inside another function. This is the simplest form of function invocation.

• 🧠 Method Invocation: When a function is called as a property of an object, it executes in the context of that object, allowing access to its properties through this. This distinction is important for object-oriented programming in JavaScript.

• 🧠 Constructor Invocation: Using the new keyword creates a new object, and this within that function refers to the newly created object. This is fundamental for creating instances of objects.

image.png

08 - The this argument values

The execution context in JavaScript determines the value of this in function calls, varying by the method of invocation.

Highlights

Key Insights

🌐 Execution Context: Every function call in JavaScript occurs within a specific context that includes variables and scope information, essential for proper execution. Understanding this context is crucial for debugging and writing effective code.

🔗 The this Keyword: This keyword behaves differently based on how a function is called, making it a pivotal concept for JavaScript developers. Knowledge of this is vital for object-oriented programming in JavaScript.

🎯 Direct Function Calls: When a function is invoked directly (e.g., func()), this points to the global object, which highlights how context can vary widely between environments (browser vs. Node.js).

🏷️ Method Calls: When a function is called as a property of an object (e.g., obj.func()), this refers to that object. This showcases how object-oriented principles manifest in JavaScript.

🆕 Constructor Functions: Using the new keyword creates a new instance where this refers to the newly created object, illustrating JavaScript’s prototypal inheritance model.

🔄 Method Variability: The value of this is predictable based on the function invocation method, making it easier to anticipate behavior and avoid bugs in code.

⚡ Next Steps: Understanding the fourth method of function calls and practical applications of this is essential for mastering JavaScript’s execution context and resolving common issues related to scope.

09 - Working on objects with this reference

function Bicycle(cadence, speed, gear, tirePressure) {
  this.cadence = cadence;
  this.speed = speed;
  this.gear = gear;
  this.tirePressure = tirePressure;
  this.inflateTires = function () {
    this.tirePressure += 3;
  };
}

// Creating an instance of Bicycle
var bicycle1 = new Bicycle(50, 20, 4, 25);

// Calling the method to increase tire pressure
bicycle1.inflateTires();

Over here the Bicycle constructor function demonstrates how objects in JavaScript can have properties and methods that operate within the context of the object they belong to. The inflateTires method is a particularly important example because it highlights how the this keyword works in JavaScript and how it enables the modification of object-specific properties.

When a new Bicycle instance is created using the Bicycle constructor function, the this keyword inside the function refers to the newly created object. For example, when bicycle1 is instantiated using the new Bicycle(50, 20, 4, 25) call, this in the constructor points to bicycle1. The constructor assigns the passed values to the object's properties (cadence, speed, gear, and tirePressure) and defines the inflateTires method directly on the object.

The inflateTires method uses this.tirePressure to access and modify the tirePressure property of the object it is called on. When the method is invoked on an instance, such as bicycle1.inflateTires(), the this keyword inside the method dynamically refers to the bicycle1 instance. This is a crucial feature of JavaScript's this behavior: its value is determined by the object that calls the method.

var bicycle1 = new Bicycle(50, 20, 4, 25);
console.log(bicycle1.tirePressure); // Output: 25

bicycle1.inflateTires();
console.log(bicycle1.tirePressure); // Output: 28

Here, calling bicycle1.inflateTires() increases bicycle1's tirePressure by 3, because the this keyword inside the method specifically refers to the bicycle1 instance. If the method were called on a different object (e.g., another Bicycle instance), this would refer to that object, and only its tirePressure would be updated.

This behavior demonstrates how JavaScript allows methods to operate within the scope of the object they belong to, providing a powerful way to manage object-specific data. By using the this keyword, methods like inflateTires can dynamically adapt to the context of the object they are called on, ensuring that changes are made only to the relevant instance. This encapsulation of behavior and data is fundamental to object-oriented programming in JavaScript.

10 - Using the call function

In the video, the concept of function context in JavaScript is explored through a practical example of creating a Mechanic object that borrows the inflateTires method from a Bicycle object. This example emphasizes the importance of understanding how the this keyword behaves in different contexts and how to manage it effectively when reusing methods across objects.

Code Explanation:

  1. Bicycle Constructor: A Bicycle constructor function is defined, allowing the creation of bicycle objects with properties like cadence, speed, gear, and tirePressure. Each Bicycle instance has an inflateTires method that increases the tirePressure by 3, using this.tirePressure to refer to the specific object it is called on.
  2. Mechanic Constructor: A Mechanic constructor function is used to create mechanic objects with a name property. In this example, a mechanic named "Mike" is created.
  3. Method Borrowing: The inflateTires method from the bicycle1 object is assigned to the Mechanic instance mike. However, when mike.inflateTires() is invoked, the this keyword inside the method refers to the mike object, which does not have a tirePressure property. This results in an error or an invalid operation (e.g., this.tirePressure += 3 will produce NaN because undefined + 3 is not a valid operation).

Fixing the Context Issue:

To address this, we need to ensure that the this keyword inside the inflateTires method refers to the correct object (e.g., a Bicycle instance). This can be done in two ways:

  1. Passing the Bicycle Object as an Argument: Modify inflateTires to accept an object as an argument and directly update its tirePressure.
  2. Binding the Context Explicitly: Use the call method to explicitly set the value of this to the bicycle1 object when invoking inflateTires.

Here’s the final code that demonstrates both approaches:

Code:

// Bicycle constructor
function Bicycle(cadence, speed, gear, tirePressure) {
  this.cadence = cadence;
  this.speed = speed;
  this.gear = gear;
  this.tirePressure = tirePressure;
  this.inflateTires = function () {
    this.tirePressure += 3; // This assumes 'this' points to a Bicycle object.
  };
}

// Mechanic constructor
function Mechanic(name) {
  this.name = name;
}

// Creating objects
var bicycle1 = new Bicycle(50, 20, 4, 25);
var mike = new Mechanic("Mike");

// Borrowing the inflateTires method
mike.inflateTires = bicycle1.inflateTires;

// Fix 1: Pass the Bicycle object as an argument
mike.inflateTires = function (bicycle) {
  bicycle.tirePressure += 3;
};
mike.inflateTires(bicycle1);
console.log(bicycle1.tirePressure); // Output: 28

// Fix 2: Use call to explicitly bind the context
mike.inflateTires = bicycle1.inflateTires; // Reassign the original method
mike.inflateTires.call(bicycle1);
console.log(bicycle1.tirePressure); // Output: 31

Key Insights:

This example underscores the flexibility and challenges of working with this in JavaScript, encouraging developers to pay close attention to context when reusing or borrowing methods.


Unit 03 - Prototypes

11 - When constructors aren't good enough

Comparison with Class-Based Languages

The Problem with Constructor Functions

Drawback:

This is where Prototypes comes as a solution.

Note: There is a new class keyword in the newer version of JavaScript(ES6) that simulates class-like behaviour, but JavaScript does not have the class concept.

12 - Introducing the prototype

  1. Functions in JavaScript Are Objects:

  2. Accessing Function Objects and Prototype Objects:

  3. Prototype Object Creation:

  4. Using the new Keyword:

    image.png

  5. The Role of __proto__:

  6. Behavior of the Prototype Object:

  7. Key Observations:

These steps are fundamental for understanding how JavaScript utilizes prototypes to manage object behavior and inheritance. The significance of the prototype object and the __proto__ property will become clearer in subsequent lessons.

13 - Property lookup with prototypes

Why This Mechanism Exists?

This allows JavaScript to create shared behavior across objects (like a blueprint or template). Instead of duplicating methods and properties for every instance, shared behaviors reside in the prototype. More on this will be explored in future lessons!

14 - Object behaviors using prototypes

Why Prototype Lookup Exists

Prototype in Constructor Functions

Objects created using the new keyword inherit from the constructor’s .prototype. Example: Constructor with a Prototype

function Employee(name) {
  this.name = name; // Instance-specific property
}

// Adding shared behavior to the prototype
Employee.prototype.playPranks = function () {
  console.log("Prank played!");
};

// Create objects
const emp1 = new Employee("Jim");
const emp2 = new Employee("Pam");

// Access shared method
emp1.playPranks(); // "Prank played!"
emp2.playPranks(); // "Prank played!"

// Check property ownership
console.log(emp1.hasOwnProperty("playPranks")); // false (comes from prototype)
// Inefficient Method Definition:
function Employee(name) {
  this.name = name;
  this.playPranks = function () {
    console.log("Prank played!");
  };
}

const emp1 = new Employee("Jim");
const emp2 = new Employee("Pam");

console.log(emp1.playPranks === emp2.playPranks); // false (different copies)

---------------------------------------------------------------
// Efficient Method Definition:
function Employee(name) {
  this.name = name;
}
Employee.prototype.playPranks = function () {
  console.log("Prank played!");
};

const emp1 = new Employee("Jim");
const emp2 = new Employee("Pam");

console.log(emp1.playPranks === emp2.playPranks); // true (shared method)

Dynamic Prototype Modifications

// Add a method to the prototype after object creation
Employee.prototype.greet = function () {
  console.log(`Hello, ${this.name}!`);
};

emp1.greet(); // "Hello, Jim!"
emp2.greet(); // "Hello, Pam!"

Key Characteristics of Prototype Behavior

  1. Dynamic Runtime Lookup: Prototype properties are checked at runtime, so changes to the prototype are immediately reflected on all objects.
  2. Shared Behavior: Shared methods reduce memory usage and simplify updates.
  3. No Need for Upfront Definition: Unlike class-based languages, prototype methods can be added dynamically after object creation.
  4. In traditional class-based languages, all behaviors must be defined upfront before object creation.

Note: The double underscores are referred to as “dunder” as in “Dunder Mifflin”. So this property is called “dunder-proto”.

In JavaScript, objects and functions are connected via a network of prototype relationships that allow behavior sharing and object creation. Here's how it works:

This setup allows an object to "inherit" behaviors defined on the prototype:

Foo.prototype.greet = function () {
  console.log("Hello!");
};

a.greet(); // "Hello!"
// If the object (a) doesn’t have a property or method, JavaScript looks up the chain to __proto__ (i.e., the prototype) to find it.

You can even use the constructor to create new objects:

const b = new a.__proto__.constructor();
console.log(b instanceof Foo); // true
// While this works, it’s not recommended because modifying __proto__ or constructor can lead to unexpected behavior:
a.__proto__.constructor = function Bar() {};
console.log(a.__proto__.constructor); // Bar
// Changing constructor breaks the relationship between the object and its original creator.

Best Practices

Instead of relying on __proto__, use the following:

When defining shared behavior, always use the constructor’s prototype property:

Foo.prototype.sayHello = function () {
  console.log("Hello from Foo!");
};

const c = new Foo();
c.sayHello(); // "Hello from Foo!"

16 - The Object function

The Object Function in JavaScript

The Object function in JavaScript is both a global function and an object. It acts as a global constructor function that allows you to create objects. For example:

// Simplest way to create an object
let simple = {};

// Another way to create an object using the Object function
let obj = new Object();

Both approaches are equivalent. {} is simply a shorthand for new Object(). To prove this, you can check the prototype chain:

console.log(simple.__proto__ === obj.__proto__); // true

When you create an object using {}, JavaScript internally calls new Object() behind the scenes.

17 - The Prototype object

18 - Inheritance In JavaScript

In JavaScript, inheritance is achieved via the prototype chain, allowing objects to inherit properties and methods from other objects. This example demonstrates how to implement multi-level inheritance using JavaScript's prototype system.

This concept is the foundation of object-oriented programming in JavaScript and can be extended further to create deep inheritance hierarchies.

Thank you!