Join our WhatsApp Channel
Cart / 0.00$

No products in the cart.

Visit Support Portal
No Result
View All Result
Friday, July 31, 2026
  • Home
  • Tutorials
    • Machine Learning
    • Python
    • Web Development
    • Javascript
    • Mathematics
    • Deep Learning
    • Artificial Intelligence
  • Store
  • Our ServicesBook Now
    • Managed Hosting Services
    • Web Development Services
  • Our Softwares
    • JaggoRe AIFree
    • Email ExtractorPaid
    • TimeWell AppFree
Get a Free Quote
  • Login
  • Register
Neuraldemy
Friday, July 31, 2026
Cart / 0.00$

No products in the cart.

No Result
View All Result
Neuraldemy
Get A Free Quote
Home Javascript

JavaScript Concepts Revision For Interview

Neuraldemy by Neuraldemy
July 30, 2026
in Javascript, Web Development
Reading Time: 259 mins read
A A
Javascript Tutorial

In this tutorial, we will do JavaScript concepts revision for interview. If you already know JavaScript and you just want to revise important concepts then this is the right tutorial for you. Let’s get started.

Table of Contents

  • 1. Variables, Scope, and the Temporal Dead Zone (TDZ)
    • Scope
    • Hoisting and the TDZ
    • let vs. const
    • 1. The “Shadowing and TDZ” Trap
    • 2. The “Function Hoisting” Trap
    • 3. The “Classic Loop Fix” (Without let)
  • 2. Data Types, Equality, and Type Coercion
    • Primitives (Pass by Value)
      • 1. String
      • 2. Number
      • 3. Boolean
      • 4. Undefined
      • 5. Null
      • 6. Symbol (ES6)
      • 7. BigInt
      • NaN
    • Avoid using the global isNaN()
    • Non-Primitives (Pass by Reference)
    • Equality: == vs ===
    • Type Coercion
      • Rule 1: If either operand is a string, + performs string concatenation
      • Rule 2: -, *, /, % always try to convert operands to numbers
      • Rule 3: Boolean converts to numbers in arithmetic
      • Rule 4: null becomes 0 in numeric operations
      • Rule 6: Equality (==) performs type coercion
      • Rule 7: Strict equality (===) never performs type coercion
      • Rule 8: Empty string
      • Rule 9: Arrays
      • Rule 10: Objects
      • Rule 11: Boolean conversion (Truthy & Falsy)
      • Rule 12: Explicit coercion
      • Rule 13: NaN
      • 1. The typeof quirks
      • 2. The Bizarre Coercion Puzzle
  • 3. Operators In JavaScript
    • Arithmetic Operators
      • Addition (+)
      • Subtraction (-)
      • Multiplication (*)
      • Division (/)
      • The Modulo Operator (%)
      • Exponentiation (**)
      • Increment (++)
        • Post Increment
        • Pre Increment
      • Decrement (–)
        • Post Decrement
        • Pre Decrement
      • Unary Plus (+)
        • Unary Minus (-)
    • Assignment Operators
    • Comparison Operators
      • Equality (==)
      • Strict Equality (===)
      • Inequality (!=)
      • Strict Inequality (!==)
      • Greater Than (>)
      • Less Than (<)
      • Greater Than or Equal (>=)
      • Less Than or Equal (<=)
        • String Comparison
        • Object Comparison
        • Special Cases
    • Logical Operators
      • Logical AND (&&)
        • Short-Circuit Evaluation with &&
        • && Returns Values, Not Just Booleans. Many beginners think && always returns true or false, but it actually returns one of its operands.
      • Logical OR (||)
        • Short-Circuit Evaluation with ||
        • || Returns Values, Not Just Booleans
      • Logical NOT (!)
        • Double NOT (!!)
    • Nullish Coalescing Operator (??)
      • Operator Precedence
    • Assignment Operator With Above Concepts
      • Logical AND Assignment (&&=)
      • Logical OR Assignment (||=)
    • Nullish Coalescing Assignment (??=)
    • Conditional (Ternary) Operator
      • Basic Example
      • Using Expressions
      • Using Function Calls
      • Nested Ternary Operator
      • Ternary with Logical Operators
      • Truthy and Falsy Values
      • Returning Objects
    • JavaScript-Specific Operators
      • typeof Operator
      • delete Operator
      • in Operator
      • instanceof Operator
      • new Operator
      • Optional Chaining (?.)
      • Nullish Coalescing with Optional Chaining
      • void Operator
      • Comma Operator (,)
    • Bitwise Operators
      • Bitwise OR (|)
      • Bitwise XOR (^)
      • Bitwise NOT (~)
      • Left Shift (<<)
      • Right Shift (>>)
      • Unsigned Right Shift (>>>)
      • Bitwise Assignment Operators
  • 4. Control Flow
    • if, else if, else
    • switch
    • for Loop
    • while Loop
    • do…while Loop
    • for…of
    • for…in
    • break
    • continue
    • Labels
  • 5. Numbers and the Number Object
    • Number.MAX_VALUE
    • Number.MIN_VALUE
    • Number.MAX_SAFE_INTEGER
    • Number.EPSILON
    • parseInt() vs Number()
    • Questions:
    • Questions
  • 6. Strings and the String Object
  • 7. Arrays
    • forEach()
      • Purpose
      • Syntax
    • map()
      • Syntax
    • filter()
      • Syntax
    • reduce()
      • Syntax
    • sort()
      • Default Behavior
      • Comparator Function
    • some()
    • every()
  • 8. Objects
    • Prototype & Prototype Chain
      • __proto__ vs prototype
      • Constructor Functions & the new Keyword
      • What Happens Without new?
      • 1. prototype (Blueprint)
      • 2. __proto__ (The Link)
      • 1. Object (The Built-in Function)
      • 2. {} (The Object Instance)
    • ES6 classes
      • 1. The Basic Class (Constructors & Prototypes)
      • 2. Inheritance (extends and super)
    • Shallow vs Deep Copy
      • Shallow Copy (The Surface Clone)
      • Deep Copy (The Total Clone)
  • 9. This Keyword
    • 1. Implicit Binding (The “Dot” Rule)
      • 2. Explicit Binding (The call, apply, bind Rule)
      • 3. New Binding (The new Keyword Rule)
      • 4. Default Binding (The Global/Standalone Rule)
      • The Exception: Arrow Functions (=>)
  • 10. Asynchronous JavaScript
    • 1. The Event Loop Architecture
  • 11. Advanced Functions & Execution Context
    • Phase 1: The Creation Phase (Memory Setup)
    • Phase 2: The Execution Phase (Running the Code)
    • Closures (The Backpack)
    • Currying
  • IIFE (Immediately Invoked Function Expressions)
  • Modern Data Structures & Advanced Patterns (ES6+)
    • 1. Map and WeakMap
    • 2. Set and WeakSet
    • 3. Iterators and Generators
    • The Syntax
    • Debounce & Throttle
    • Debouncing (The “Elevator” Pattern)
    • Throttling (The “Machine Gun” Pattern)
  • 12. Modules: CommonJS vs. ESM
    • System A: CommonJS (The Node.js Pioneer)
    • System B: ECMAScript Modules (ESM – The Modern Standard)
      • 1. Named Exports (For multiple items)
      • 2. Default Exports (For a single main item)
    • 4. Dynamic Imports (Lazy Loading)
    • 6. Why ESM Won (Tree Shaking)
  • 13. Typed Arrays
    • Step 1: ArrayBuffer (The Memory Block)
    • Step 2: The View (How you read it)

1. Variables, Scope, and the Temporal Dead Zone (TDZ)

In modern JavaScript (ES6 and beyond), we use let and const instead of the legacy var. To understand why, and to answer common interview questions, you have to understand scope and hoisting.

Scope

Scope determines where your variables are accessible.

  • Global Scope: Accessible everywhere.
  • Function Scope: Accessible only within the function they are declared in.
  • Block Scope: Accessible only within the { } block they are declared in (like an if statement or a for loop).

var is function-scoped. let and const are block-scoped.

function scopeExample() {
  if (true) {
    var oldWay = "I leak out of the block!";
    let newWay = "I am trapped in the block!";
    const alsoNew = "Me too, and I cannot be reassigned!";
  }
  
  console.log(oldWay); // Output: "I leak out of the block!"
  console.log(newWay); // ReferenceError: newWay is not defined
}

Hoisting and the TDZ

Hoisting is JavaScript’s default behavior of moving declarations to the top of the current scope during the compile phase, before the code executes.

This is where interviewers love to test you.

console.log(myVar); // Output: undefined
var myVar = "Hello"; 

console.log(myLet); // ReferenceError: Cannot access 'myLet' before initialization
let myLet = "World";

Why does let throw an error while var prints undefined? Both var and let are hoisted to the top of their scope. However, var is initialized with undefined immediately. let and const are hoisted, but they remain uninitialized.

The time between the start of the block and the actual line where the variable is declared is called the Temporal Dead Zone (TDZ). Accessing a variable in its TDZ throws a ReferenceError. This was introduced intentionally to catch bugs caused by using variables before they are ready.

let vs. const

  • Use const by default. It signals that the variable’s reference will not change.
  • Use let only when you know the value will be reassigned (like a counter in a loop or a status toggle).
  • Important: const does not make objects or arrays immutable. It only prevents reassignment of the variable name itself.
const user = { role: "viewer" };
user.role = "editor"; // This is perfectly fine! The object mutated.
user = { role: "admin" }; // TypeError: Assignment to constant variable.

An interviewer might hand you this code and ask what it outputs, and why:

for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}

Because var is not block-scoped, there is only one i in memory. By the time the setTimeout finishes 1 second later, the loop has already finished, and i is 3. It prints 3, 3, 3.

If you change var to let, let creates a new block scope for every iteration of the loop. It will correctly print 0, 1, 2.

1. The “Shadowing and TDZ” Trap

The Code:

let x = 10;

if (true) {
  console.log(x);
  let x = 20;
}

The Output: ReferenceError: Cannot access 'x' before initialization

The Explanation: You might expect it to print 10 because the global x is defined before the console.log. However, let and const are block-scoped and hoisted to the top of their block.

When the engine enters the if block, it sees let x = 20; and hoists the declaration of the local x to the top of that block. This local x “shadows” (hides) the global x. But because let variables sit in the Temporal Dead Zone (TDZ) until their actual line of execution, trying to console.log(x) throws an error.

2. The “Function Hoisting” Trap

The Code:

var a = 1;

function b() {
  a = 10;
  return;
  function a() {}
}

b();
console.log(a);

The Output: 1

The Explanation: This looks incredibly confusing until you read it the way the JavaScript compiler does. Function declarations (function a() {}) are fully hoisted to the very top of their scope. Inside function b, the engine hoists function a() {} right to the top.

Here is how the engine interprets function b:

function b() {
  var a = function() {}; // Hoisted from below
  a = 10;                // Reassigns the LOCAL 'a', not the global one
  return;
}

Because the local function declaration created a local variable named a, a = 10; only affects the local scope. The global a remains 1.

3. The “Classic Loop Fix” (Without let)

The Code: In the previous setTimeout example, we fixed the 3, 3, 3 problem by changing var to let. The interviewer will often follow up with: “Now fix it without using let or const.”

The Solution:

for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(() => {
      console.log(j);
    }, 1000);
  })(i);
}

The Explanation: We wrap the setTimeout in an Immediately Invoked Function Expression (IIFE). Every time the loop runs, we immediately call the anonymous function and pass the current value of i into it. Functions in JavaScript create their own scope. So, the parameter j becomes a unique, local variable for each iteration, locking in the value of i at that specific moment in time.

2. Data Types, Equality, and Type Coercion

JavaScript has two main categories of data types: Primitives and Non-Primitives (Objects/References).

Primitives (Pass by Value)

There are 7 primitive types in modern JavaScript:

  1. String
  2. Number
  3. Boolean
  4. Undefined (Variable declared but not assigned a value)
  5. Null (Intentional absence of any value)
  6. Symbol (Unique identifiers, introduced in ES6)
  7. BigInt (For numbers larger than the Number type can safely hold)

Primitives are stored by value. When you assign a primitive to a new variable, JavaScript creates a complete copy of that value.

let status = "active";
let newStatus = status; 
newStatus = "archived";

console.log(status);     // Output: "active"
console.log(newStatus);  // Output: "archived"

1. String

Used to represent text.

let name = "Alice";
let city = 'Delhi';

console.log(name); // Alice
console.log(typeof name); // string

Strings can use single quotes, double quotes, or backticks.


2. Number

Represents both integers and floating-point numbers.

let age = 25;
let price = 99.99;

console.log(age); // 25
console.log(price); // 99.99
console.log(typeof age); // number

JavaScript has only one number type for integers and decimals.


3. Boolean

Represents only two values: true or false.

let isLoggedIn = true;
let hasPermission = false;

console.log(isLoggedIn); // true
console.log(typeof isLoggedIn); // boolean

Booleans are mainly used in conditions.

if (isLoggedIn) {
    console.log("Welcome!");
}

4. Undefined

A variable has the value undefined when it is declared but not assigned.

let x;

console.log(x); // undefined
console.log(typeof x); // undefined

Another example:

function test() {}

console.log(test()); // undefined

Since test() doesn’t return anything, JavaScript returns undefined.


5. Null

Represents the intentional absence of a value.

let user = null;

console.log(user); // null
console.log(typeof user); // object (this is a long-standing JavaScript bug preserved for backward compatibility)

Difference between null and undefined:

let a;
let b = null;

console.log(a); // undefined (not assigned)
console.log(b); // null (explicitly empty)

6. Symbol (ES6)

Creates a unique value.

const id1 = Symbol("id");
const id2 = Symbol("id");

console.log(id1 === id2); // false
console.log(typeof id1); // symbol

Even though both have the same description ("id"), they are completely unique.

Symbols are often used as unique object property keys.

const key = Symbol("id");

const user = {
    [key]: 123
};

console.log(user[key]); // 123

7. BigInt

Used for integers larger than Number.MAX_SAFE_INTEGER.

const big = 1234567890123456789012345678901234567890n;

console.log(big);
console.log(typeof big); // bigint

Without BigInt, large numbers lose precision.

console.log(9007199254740991);     // Safe
console.log(9007199254740992);     // May lose precision
console.log(9007199254740992n);    // Exact BigInt

NaN

NaN is a primitive value of the number type.

let x = NaN;

console.log(x);          // NaN
console.log(typeof x);   // "number"

NaN stands for Not-a-Number, but despite its name, its type is number.

It usually results from invalid numeric operations, for example:

console.log(0 / 0);          // NaN
console.log(Number("abc"));  // NaN
console.log(Math.sqrt(-1));  // NaN

One interesting property of NaN is that it is not equal to itself:

console.log(NaN === NaN); // false
console.log(NaN == NaN);  // false

To check if a value is NaN, use:

console.log(Number.isNaN(NaN));      // true
console.log(Number.isNaN("hello"));  // false

Avoid using the global isNaN()

console.log(isNaN("hello"));        // true (because it converts "hello" to NaN)
console.log(Number.isNaN("hello")); // false
console.log(Number.isNaN(NaN));     // true

Number.isNaN() is preferred because it only returns true if the value is actually NaN.

Non-Primitives (Pass by Reference)

Objects, Arrays, and Functions are non-primitives. They are stored by reference. When you assign an object to a new variable, you are not copying the object itself; you are copying the pointer to the location in memory where that object lives.

let adminUser = { role: "editor", isArchived: false };
let updatedUser = adminUser; 

updatedUser.isArchived = true;

// Both variables point to the exact same object in memory!
console.log(adminUser.isArchived); // Output: true

Interview Tip: If an interviewer asks how to fix this so that modifying updatedUser doesn’t mutate adminUser, you would use the spread operator to create a shallow copy: let updatedUser = { ...adminUser };.

Equality: == vs ===

  • === (Strict Equality): Checks if both the value and the type are exactly the same. Always use this by default.
  • == (Loose Equality): Checks if the values are the same after JavaScript attempts to convert them to the same type (Type Coercion).
console.log(1 == "1");  // true (String "1" is coerced into Number 1)
console.log(1 === "1"); // false (Different types)

Type Coercion

Type coercion is JavaScript’s automatic conversion of a value from one data type to another when an operation involves different types.

There are two kinds:

  • Implicit coercion – JavaScript converts types automatically.
  • Explicit coercion – You convert types yourself using functions like Number(), String(), or Boolean().
Rule 1: If either operand is a string, + performs string concatenation
console.log("5" + 2);      // "52"
console.log(5 + "2");      // "52"
console.log("Hello " + 5); // "Hello 5"

The + operator sees a string, so it converts the other operand to a string.


Rule 2: -, *, /, % always try to convert operands to numbers
console.log("5" - 2); // 3
console.log("5" * 2); // 10
console.log("8" / 2); // 4
console.log("7" % 3); // 1

Even though "5" is a string, these operators expect numbers.

If conversion fails:

console.log("abc" - 2); // NaN
console.log("abc" * 2); // NaN
Rule 3: Boolean converts to numbers in arithmetic
console.log(true + 1);   // 2
console.log(false + 1);  // 1
console.log(true * 10);  // 10

JavaScript converts:

true  → 1
false → 0

Rule 4: null becomes 0 in numeric operations
console.log(null + 5); // 5
console.log(null * 4); // 0

because

Number(null) === 0
Rule 6: Equality (==) performs type coercion
console.log(5 == "5");      // true
console.log(true == 1);     // true
console.log(false == 0);    // true
console.log(null == undefined); // true

JavaScript converts values before comparing.

Rule 7: Strict equality (===) never performs type coercion
console.log(5 === "5"); // false
console.log(true === 1); // false
console.log(0 === false); // false

It compares both value and type.

Rule 8: Empty string
console.log("" + 5); // "5"
console.log("" == 0); // true
console.log("" * 5); // 0

because

Number("") === 0
Rule 9: Arrays

Empty array:

console.log([] + []); // ""

Explanation:

[]

↓

""

+

""

↓

""

Array with values:

console.log([1,2] + [3,4]);

Output

"1,23,4"

because arrays become strings.

[1,2]

↓

"1,2"

+

"3,4"

↓

"1,23,4"
Rule 10: Objects
console.log({} + {});

Output

"[object Object][object Object]"

Objects convert to strings.

Rule 11: Boolean conversion (Truthy & Falsy)

Falsy values:

false
0
-0
0n
""
null
undefined
NaN

Everything else is truthy.

Example:

if ("hello") {
    console.log("Runs");
}

Output

Runs

because non-empty strings are truthy.

Another example:

if (0) {
    console.log("Runs");
} else {
    console.log("Doesn't run");
}

Output

Doesn't run
Rule 12: Explicit coercion

Convert to Number

console.log(Number("123")); // 123
console.log(Number(true));  // 1
console.log(Number(false)); // 0
console.log(Number(null));  // 0
console.log(Number("abc")); // NaN

Convert to String

console.log(String(123)); // "123"
console.log(String(true)); // "true"

Convert to Boolean

console.log(Boolean(1)); // true
console.log(Boolean(0)); // false
console.log(Boolean("")); // false
console.log(Boolean("Hello")); // true
Rule 13: NaN
console.log(NaN == NaN);   // false
console.log(NaN === NaN);  // false
console.log(Number.isNaN(NaN)); // true

Here are two classic questions on Data Types and Coercion.

1. The typeof quirks

The Question: What do the following statements output?

console.log(typeof null);
console.log(typeof NaN);
console.log(typeof []);

The Answer:

  • typeof null outputs "object". (This is a famous, unfixable bug in the original JavaScript implementation from 1995!).
  • typeof NaN outputs "number". (Yes, “Not a Number” is technically of the numeric data type).
  • typeof [] outputs "object". (Arrays are just specialized objects under the hood). To check if something is truly an array, use Array.isArray([]).
2. The Bizarre Coercion Puzzle

The Question: What does [] == ![] evaluate to, and why?

The Answer: It evaluates to true. This is a brutal test of the coercion sequence:

  1. The ! operator forces a boolean context. Arrays are “truthy”, so ![] becomes false.
  2. Now we have [] == false.
  3. When comparing different types with loose equality, JavaScript tries to convert them to numbers. false becomes 0.
  4. Now we have [] == 0.
  5. The empty array is coerced to a primitive string "", which is then coerced to a number 0.
  6. Finally, 0 == 0 is true.

For more on this read here.

3. Operators In JavaScript

Arithmetic Operators

These perform mathematical operations.

Addition (+)

Adds numbers or concatenates strings.

console.log(10 + 5);      // 15
console.log("Hello " + "World"); // Hello World
console.log("10" + 5);    // "105"

Notice the last example: if one operand is a string, + performs string concatenation instead of numeric addition.

Subtraction (-)

Always converts operands to numbers.

console.log(10 - 5);      // 5
console.log("10" - 5);    // 5
console.log(true - 1);    // 0
console.log("abc" - 1);   // NaN

Multiplication (*)

console.log(4 * 5);       // 20
console.log("4" * 5);     // 20
console.log(true * 5);    // 5
console.log(false * 5);   // 0

Division (/)

console.log(10 / 2);      // 5
console.log(7 / 2);       // 3.5
console.log("8" / 2);     // 4

console.log(10 / 0);      // Infinity
console.log(-10 / 0);     // -Infinity
console.log(0 / 0);       // NaN

The Modulo Operator (%)

It returns the remainder of a division. It is heavily used in interviews to determine if a number is even or odd, or to cycle through a list.

console.log(10 % 3); // Output: 1 (Because 10 / 3 is 3 with a remainder of 1)
console.log(4 % 2);  // Output: 0 (Even numbers always return 0 when modulo 2)

Exponentiation (**)

Raises a number to a power.

console.log(2 ** 3); // 8
console.log(5 ** 2); // 25
console.log(10 ** 0); // 1

Equivalent to:

Math.pow(2, 3);

Increment (++)

Increases a value by one.

Post Increment
let a = 5;

console.log(a++); // 5
console.log(a);   // 6

Returns the current value, then increments.

Pre Increment
let a = 5;

console.log(++a); // 6
console.log(a);   // 6

Increments first, then returns the new value.

Decrement (--)

Post Decrement
let a = 5;

console.log(a--); // 5
console.log(a);   // 4
Pre Decrement
let a = 5;

console.log(--a); // 4
console.log(a);   // 4

Unary Plus (+)

Converts a value to a number.

console.log(+"10");      // 10
console.log(+true);      // 1
console.log(+false);     // 0
console.log(+null);      // 0
console.log(+undefined); // NaN

Equivalent to:

Number("10");
Unary Minus (-)

Converts to a number and negates it.

console.log(-"10");   // -10
console.log(-true);   // -1
console.log(-false);  // 0

Assignment Operators

Assignment operators are used to assign values to variables. Besides the basic assignment operator (=), JavaScript provides shorthand assignment operators that combine an operation with assignment.

Basic assignment:

let x = 10;

Compound assignments:

let x = 10;

x += 5;   // x = x + 5
console.log(x); // 15

x -= 2;   // 13
x *= 2;   // 26
x /= 2;   // 13
x %= 5;   // 3
x **= 2;  // 9

Operator Precedence

console.log(2 + 3 * 4); // 14

Multiplication happens before addition.

Equivalent to:

2 + (3 * 4)

Use parentheses to override precedence.

console.log((2 + 3) * 4); // 20

Comparison Operators

Comparison operators compare two values and return a boolean (true or false). They are commonly used in if statements, loops, and conditional expressions.

console.log(10 > 5);   // true
console.log(10 < 5);   // false

Equality (==)

The loose equality operator compares two values after performing type coercion if necessary.

console.log(5 == 5);      // true
console.log(5 == "5");    // true
console.log(true == 1);   // true
console.log(false == 0);  // true
console.log(null == undefined); // true
console.log("" == 0); // true

Strict Equality (===)

The strict equality operator compares both the value and the data type. No type coercion occurs.

console.log(5 === 5);      // true
console.log(5 === "5");    // false
console.log(true === 1);   // false
console.log(false === 0);  // false
console.log(null === undefined); // false

This is generally the preferred equality operator because it avoids unexpected type conversions.

Inequality (!=)

The loose inequality operator checks whether two values are not equal, using type coercion.

console.log(5 != 10);     // true
console.log(5 != "5");    // false
console.log(true != 1);   // false
console.log(null != undefined); // false

Strict Inequality (!==)

The strict inequality operator compares both value and type without coercion.

console.log(5 !== "5"); // true
console.log(5 !== 5);   // false
console.log(true !== 1); // true

Greater Than (>)

Checks whether the left operand is greater than the right operand.

console.log(10 > 5);   // true
console.log(5 > 10);   // false
console.log("20" > 10); // true

Less Than (<)

Checks whether the left operand is less than the right operand.

console.log(5 < 10);   // true
console.log(10 < 5);   // false
console.log("5" < 10); // true

Greater Than or Equal (>=)

Returns true if the left operand is greater than or equal to the right operand.

console.log(10 >= 10); // true
console.log(15 >= 10); // true
console.log(5 >= 10);  // false

Less Than or Equal (<=)

Returns true if the left operand is less than or equal to the right operand.

console.log(10 <= 10); // true
console.log(5 <= 10);  // true
console.log(15 <= 10); // false
String Comparison

Strings are compared lexicographically (dictionary order) based on their Unicode values.

console.log("apple" < "banana"); // true
console.log("cat" > "bat");      // true
console.log("abc" < "abd");      // true

Comparison is case-sensitive.

console.log("A" < "a"); // true
console.log("Z" < "a"); // true

This happens because uppercase letters have smaller Unicode values than lowercase letters.

Object Comparison

Objects are compared by reference, not by their contents.

let obj1 = { name: "John" };
let obj2 = { name: "John" };

console.log(obj1 == obj2);   // false
console.log(obj1 === obj2);  // false

Although both objects contain the same data, they occupy different locations in memory.

If two variables reference the same object:

let obj1 = { name: "John" };
let obj2 = obj1;

console.log(obj1 === obj2); // true

Both variables point to the same object.

Special Cases
console.log(NaN == NaN);     // false
console.log(NaN === NaN);    // false

console.log(null == undefined);   // true
console.log(null === undefined);  // false

console.log([] == false);    // true
console.log("" == false);    // true
console.log([] === false);   // false

These results occur because == performs type coercion, while === does not. Use === instead of == in most situations. Use !== instead of !=. Only use == if you intentionally want JavaScript’s type coercion behavior and fully understand its rules.

Logical Operators

Logical operators are used to combine or invert boolean expressions. They are heavily used in if statements, loops, and conditional expressions.

The three primary logical operators are:

  • && (Logical AND)
  • || (Logical OR)
  • ! (Logical NOT)

JavaScript also has the Nullish Coalescing Operator (??), which is closely related and commonly used with them.

Logical AND (&&)

The && operator returns true only if both operands are truthy.

console.log(true && true);   // true
console.log(true && false);  // false
console.log(false && true);  // false
console.log(false && false); // false

Example with conditions:

let age = 20;
let hasLicense = true;

if (age >= 18 && hasLicense) {
    console.log("Can drive");
}

Both conditions must be true.

Short-Circuit Evaluation with &&

&& stops evaluating as soon as it finds the first falsy value.

console.log(false && console.log("Hello"));

Output:

false

console.log("Hello") is never executed.

Example:

let user = null;

console.log(user && user.name);

Output:

null

Since user is falsy, JavaScript doesn’t try to access user.name.

&& Returns Values, Not Just Booleans. Many beginners think && always returns true or false, but it actually returns one of its operands.

This is the rule:

  • Returns the first falsy value
  • If none are falsy, returns the last value
console.log(10 && 20);      // 20
console.log(0 && 20);       // 0
console.log("" && "Hello"); // ""
console.log(true && "Hi");  // Hi

Logical OR (||)

The || operator returns true if at least one operand is truthy.

console.log(true || false); // true
console.log(false || true); // true
console.log(false || false); // false

Example:

let isAdmin = false;
let isOwner = true;

if (isAdmin || isOwner) {
    console.log("Access Granted");
}

Only one condition needs to be true.

Short-Circuit Evaluation with ||

|| stops evaluating as soon as it finds the first truthy value.

console.log(true || console.log("Hello"));

Output:

true

The second expression is never evaluated.

|| Returns Values, Not Just Booleans

Rule:

  • Returns the first truthy value
  • If none are truthy, returns the last value
console.log(0 || 100);          // 100
console.log("" || "Guest");     // Guest
console.log(null || "Default"); // Default
console.log("John" || "Guest"); // John

This is why || is commonly used to provide default values.

let username = "";

let name = username || "Guest";

console.log(name); // Guest

Logical NOT (!)

The ! operator reverses a boolean value.

console.log(!true);  // false
console.log(!false); // true

It also converts any value to a boolean before negating it.

console.log(!0);        // true
console.log(!1);        // false
console.log(!"Hello");  // false
console.log(!"");       // true
console.log(!null);     // true
Double NOT (!!)

!! converts a value to its boolean equivalent.

console.log(!!0);       // false
console.log(!!1);       // true
console.log(!!"Hello"); // true
console.log(!!"");      // false
console.log(!!null);    // false
console.log(!![]);      // true

Equivalent to:

Boolean("Hello"); // true
Boolean(0);       // false

Nullish Coalescing Operator (??)

The ?? operator returns the right operand only if the left operand is null or undefined.

console.log(null ?? "Guest");      // Guest
console.log(undefined ?? "Guest"); // Guest
console.log("John" ?? "Guest");    // John
console.log(0 ?? 100);             // 0
console.log("" ?? "Guest");        // ""

Notice the difference from ||.

Using ||:

console.log(0 || 100);   // 100
console.log("" || "Hi"); // Hi

Using ??:

console.log(0 ?? 100);   // 0
console.log("" ?? "Hi"); // ""

Use ?? when 0, false, or "" are valid values that you don’t want to replace.

Operator Precedence

Logical operators are evaluated in this order:

  1. !
  2. &&
  3. ||
  4. ?? (cannot be mixed directly with && or || without parentheses)

Example:

console.log(true || false && false);

Evaluation:

true || (false && false)

↓

true || false

↓

true

If you mix ?? with && or ||, you must use parentheses.

let value = (null ?? "Guest") || "Anonymous";

Without parentheses, JavaScript throws a SyntaxError.

Assignment Operator With Above Concepts

Logical AND Assignment (&&=)

Assigns a value only if the current value is truthy.

let username = "John";

username &&= "Alice";

console.log(username); // Alice

Since "John" is truthy, assignment occurs.

Falsy example:

let username = "";

username &&= "Alice";

console.log(username); // ""

Equivalent to:

if (username) {
    username = "Alice";
}

Logical OR Assignment (||=)

Assigns a value only if the current value is falsy.

let username = "";

username ||= "Guest";

console.log(username); // Guest

Another example:

let score = 0;

score ||= 100;

console.log(score); // 100

Because 0 is falsy, it gets replaced.

Equivalent to:

if (!score) {
    score = 100;
}

Nullish Coalescing Assignment (??=)

Assigns a value only if the current value is null or undefined.

let username = null;

username ??= "Guest";

console.log(username); // Guest

With undefined:

let value;

value ??= 50;

console.log(value); // 50

Unlike ||=, it preserves valid falsy values.

let score = 0;

score ??= 100;

console.log(score); // 0

Another example:

let message = "";

message ??= "Hello";

console.log(message); // ""

Since "" is not null or undefined, no assignment happens.

Conditional (Ternary) Operator

The ternary operator (? :) is JavaScript’s only operator that takes three operands. It is a concise alternative to an if...else statement.

Syntax:

condition ? expressionIfTrue : expressionIfFalse

If the condition is truthy, the first expression is returned; otherwise, the second expression is returned.

Basic Example
let age = 20;

let result = age >= 18 ? "Adult" : "Minor";

console.log(result); // Adult

Equivalent if...else:

let result;

if (age >= 18) {
    result = "Adult";
} else {
    result = "Minor";
}
Using Expressions

The ternary operator returns a value, so it can be used directly in assignments.

let num = 10;

let message = num % 2 === 0 ? "Even" : "Odd";

console.log(message); // Even
Using Function Calls
let isLoggedIn = true;

console.log(isLoggedIn ? "Welcome!" : "Please Login");
Nested Ternary Operator

Multiple conditions can be checked by nesting ternary operators.

let marks = 75;

let grade =
    marks >= 90 ? "A" :
    marks >= 80 ? "B" :
    marks >= 70 ? "C" :
    "Fail";

console.log(grade); // C

Although valid, deeply nested ternaries reduce readability. For many conditions, an if...else if chain is usually clearer.

Ternary with Logical Operators
let age = 25;
let hasLicense = true;

let canDrive = age >= 18 && hasLicense
    ? "Yes"
    : "No";

console.log(canDrive); // Yes
Truthy and Falsy Values

The condition doesn’t have to be a boolean. JavaScript evaluates its truthiness.

let username = "";

let message = username
    ? "Welcome " + username
    : "Guest";

console.log(message); // Guest

Since "" is falsy, the second expression is chosen.

Returning Objects
let isAdmin = true;

let user = isAdmin
    ? { role: "Admin" }
    : { role: "User" };

console.log(user.role); // Admin

The ternary operator is best used when you’re choosing between two values.

JavaScript-Specific Operators

These operators are unique to JavaScript and are frequently used in everyday development as well as interviews.

typeof Operator

The typeof operator returns the data type of a value as a string.

console.log(typeof "Hello");     // string
console.log(typeof 100);         // number
console.log(typeof true);        // boolean
console.log(typeof undefined);   // undefined
console.log(typeof Symbol());    // symbol
console.log(typeof 100n);        // bigint
console.log(typeof {});          // object
console.log(typeof []);          // object
console.log(typeof function(){});// function
console.log(typeof null); // object - bug known
delete Operator

The delete operator removes a property from an object.

let person = {
    name: "John",
    age: 25
};

delete person.age;

console.log(person);  // { name: "John" } 


let arr = [10, 20, 30];

delete arr[1];

console.log(arr); // [10, empty, 30]

// delete does not change the array length.

console.log(arr.length); // 3 

// arr.splice(1, 1); use this instead to remove
in Operator

The in operator checks whether a property exists in an object.

let person = {
    name: "John",
    age: 25
};

console.log("name" in person); // true
console.log("city" in person); // false

It also works with arrays because array indexes are object properties.

let arr = ["A", "B", "C"];

console.log(0 in arr); // true
console.log(5 in arr); // false
instanceof Operator

Checks whether an object is an instance of a constructor.

let arr = [];

console.log(arr instanceof Array);  // true
console.log(arr instanceof Object); // true

Another example:

let date = new Date();

console.log(date instanceof Date); // true

Custom constructor:

function Person(name) {
    this.name = name;
}

let user = new Person("Alice");

console.log(user instanceof Person); // true
new Operator

Creates a new object from a constructor function or class.

function Car(brand) {
    this.brand = brand;
}

let car = new Car("BMW");

console.log(car.brand);

With classes:

class Animal {
    constructor(name) {
        this.name = name;
    }
}

let dog = new Animal("Dog");

console.log(dog.name);
Optional Chaining (?.)

Safely accesses nested properties.

Without optional chaining:

let user = null;

console.log(user.name); // TypeError

Using ?.

let user = null;

console.log(user?.name); // undefined
let person = {
    address: {
        city: "Delhi"
    }
};

console.log(person?.address?.city); // Delhi
console.log(person?.contact?.phone); // undefined

Optional chaining also works with methods.

let user = {
    greet() {
        return "Hello";
    }
};

console.log(user.greet?.()); // Hello
console.log(user.login?.()); // undefined
Nullish Coalescing with Optional Chaining

These operators are commonly used together.

let user = null;

let city = user?.address?.city ?? "Unknown";

console.log(city); // Unknown
void Operator

The void operator evaluates an expression and always returns undefined.

console.log(void 0); 
console.log(void 100);
console.log(void "Hello");

It is rarely used today but may appear in older code and JavaScript interview questions.

Comma Operator (,)

The comma operator evaluates multiple expressions and returns the value of the last expression.

let result = (5, 10, 20);

console.log(result);  // 20

Another example:

let x = 1;

let y = (x++, x + 5);

console.log(x); // 2
console.log(y); // 7

The comma operator is uncommon and can reduce readability, so it’s generally avoided in application code.

Bitwise Operators

Bitwise operators work on the binary (bit) representation of numbers. Before performing the operation, JavaScript converts numbers to 32-bit signed integers.

For example, the number 5 is represented in binary as:

Decimal: 5
Binary : 00000000 00000000 00000000 00000101

The number 3 is:

Decimal: 3
Binary : 00000000 00000000 00000000 00000011

Bitwise operators compare these bits one by one.

Example:

console.log(5 & 3);

Binary representation:

5 = 0101
3 = 0011
------------
    0001      // output 1 in decimal

Another example:

console.log(12 & 10);
12 = 1100
10 = 1010
-----------
     1000   // output 8 in decimal

Common use: checking permissions or bit flags.

Bitwise OR (|)

Returns 1 if either bit is 1.

Bitwise XOR (^)

Returns 1 only if the bits are different. A number XOR itself is always 0.

Bitwise NOT (~)

Flips every bit. ~n = -(n + 1)

console.log(~5);
5  = 00000101
~5 = 11111010    // -6
Left Shift (<<)

Moves bits to the left. Each left shift by one position is approximately equivalent to multiplying by 2.

console.log(5 << 1);  // output 10 (5 *2)

// 5 = 00000101

// Shift left by 1

// 00001010   

console.log(5 << 2); // 20
console.log(8 << 1); // 16 

// 5 × 2¹ = 10
// 5 × 2² = 20
Right Shift (>>)

Moves bits to the right while preserving the sign bit. Each shift right by one position is approximately equivalent to integer division by 2.

console.log(8 >> 1);  // 4

// 8 = 00001000
// Shift right
// 00000100

console.log(20 >> 2); // 5
console.log(10 >> 1); // 5
Unsigned Right Shift (>>>)

Shifts bits to the right but always fills the leftmost bits with 0. This mainly affects negative numbers. Unlike >>, the sign bit is not preserved.

console.log(8 >>> 1);  // 4
console.log(-8 >>> 1); // 2147483644
Bitwise Assignment Operators

JavaScript provides shorthand versions.

let x = 5;

x &= 3;
console.log(x); // 1

Equivalent to:

x = x & 3;

Other assignment operators:

x |= 3;
x ^= 3;
x <<= 2;
x >>= 1;
x >>>= 1;

Check whether a number is even.

let num = 10;

console.log((num & 1) === 0); // true

// Even numbers always end with a binary 0, and odd in 1

Here is how this concept is used in real world:

const READ = 1;    // 0001
const WRITE = 2;   // 0010
const DELETE = 4;  // 0100
const ADMIN = 8;   // 1000

let permissions = READ | WRITE; // Gives a user Read and Write permissions

console.log((permissions & WRITE) !== 0); // output true; Checks if the user has Write permission:

permissions &= ~WRITE; // Removes Write permission 

permissions ^= ADMIN; // Toggles Admin permission
PatternMeaning
x & 1Check the last bit (often used to test even/odd)
x | maskTurn specific bits ON
x & ~maskTurn specific bits OFF
x ^ maskToggle specific bits
x << nMultiply by approximately 2ⁿ
x >> nDivide by approximately 2ⁿ

Many JavaScript developers go years without writing a single bitwise operation.

4. Control Flow

Control flow determines the order in which JavaScript executes statements. By default, JavaScript executes code from top to bottom, but control flow statements allow you to make decisions, repeat code, or skip parts of your program.

if, else if, else

The if statement executes a block only if its condition is truthy. If it’s false, JavaScript checks the else if conditions in order, and if none match, it executes the else block.

let marks = 82;

if (marks >= 90) {
    console.log("Grade A");
} else if (marks >= 75) {
    console.log("Grade B");
} else if (marks >= 60) {
    console.log("Grade C");
} else {
    console.log("Fail");
} 

// Grade B
switch

A switch statement is useful when comparing the same expression against multiple values. It is often cleaner than a long chain of if...else if.

let day = 3;

switch (day) {
    case 1:
        console.log("Monday");
        break;

    case 2:
        console.log("Tuesday");
        break;

    case 3:
        console.log("Wednesday");
        break;

    default:
        console.log("Invalid Day");
} 

// Wednesday

break is important. Without it, execution continues into the next case. This behavior is called fall-through. Sometimes it is useful.

for Loop

A for loop is used when you know how many times you want to repeat something.

Example:

for (let i = 1; i <= 5; i++) {
    console.log(i);
}
1
2
3
4
5

Execution order:

  1. Initialization
  2. Condition
  3. Loop body
  4. Update
  5. Repeat until condition becomes false
while Loop

A while loop checks the condition before every iteration. If the condition is false initially, the loop never runs.

let count = 1;

while (count <= 5) {
    console.log(count);
    count++;
}
1
2
3
4
5
do...while Loop

A do...while loop executes the body at least once, because the condition is checked afterward.

let count = 10;

do {
    console.log(count);    // 10
} while (count < 5);
for...of

Used to iterate over iterable values like arrays, strings, maps, and sets.

let fruits = ["Apple", "Banana", "Mango"];

for (const fruit of fruits) {
    console.log(fruit);
}

Apple
Banana
Mango
for...in

Used to iterate over the property names (keys) of an object.

let person = {
    name: "Alice",
    age: 25,
    city: "Delhi"
};

for (const key in person) {
    console.log(key, person[key]);
}

name Alice
age 25
city Delhi

Although for...in works on arrays, it is not recommended. it returns indexes, not values. For arrays, prefer for...of.

break

break immediately terminates the nearest loop or switch.

for (let i = 1; i <= 10; i++) {
    if (i === 6) {
        break;
    }

    console.log(i);
}
1
2
3
4
5
continue

continue skips the rest of the current iteration and moves to the next one.

for (let i = 1; i <= 5; i++) {
    if (i === 3) {
        continue;
    }

    console.log(i);
}
1
2
4
5
Labels

Labels allow break or continue to target an outer loop. They are rarely used but occasionally appear in interviews.

outer:
for (let i = 1; i <= 3; i++) {

    for (let j = 1; j <= 3; j++) {

        if (i === 2 && j === 2) {
            break outer;
        }

        console.log(i, j);
    }
}
1 1
1 2
1 3
2 1

Normally, break exits only the inner loop. Using a label exits the outer loop as well.

5. Numbers and the Number Object

In JavaScript, all regular numeric values (except BigInt) are of the number type. Internally, JavaScript stores numbers using the IEEE 754 64-bit floating-point format, so integers and decimals share the same data type.

// Creating numbers
let a = 42;
let b = 3.14;
let c = Number("100");
let d = new Number(10); // Wrapper object (rarely used)

console.log(typeof a); // "number"
console.log(typeof d); // "object"

// ---------- Number Properties ----------

console.log(Number.MAX_VALUE);          // Largest representable number
console.log(Number.MIN_VALUE);          // Smallest positive representable number
console.log(Number.MAX_SAFE_INTEGER);   // 9007199254740991
console.log(Number.MIN_SAFE_INTEGER);   // -9007199254740991
console.log(Number.POSITIVE_INFINITY);  // Infinity
console.log(Number.NEGATIVE_INFINITY);  // -Infinity
console.log(Number.NaN);                // NaN
console.log(Number.EPSILON);            // Smallest difference between 1 and the next representable number

// ---------- Checking Numbers ----------

console.log(Number.isInteger(10));      // true
console.log(Number.isInteger(10.5));    // false

console.log(Number.isFinite(100));      // true
console.log(Number.isFinite(Infinity)); // false

console.log(Number.isNaN(NaN));         // true
console.log(Number.isNaN("Hello"));     // false

console.log(Number.isSafeInteger(100)); // true
console.log(Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1)); // false

// ---------- Converting to Number ----------

console.log(Number("123"));      // 123
console.log(Number("12.5"));     // 12.5
console.log(Number(true));       // 1
console.log(Number(false));      // 0
console.log(Number(null));       // 0
console.log(Number(undefined));  // NaN
console.log(Number("abc"));      // NaN

// ---------- Instance Methods ----------

let num = 1234.56789;

console.log(num.toFixed(2));       // "1234.57"
console.log(num.toPrecision(4));   // "1235"
console.log(num.toExponential(2)); // "1.23e+3"
console.log(num.toString());       // "1234.56789"
console.log(num.toString(2));      // Binary
console.log(num.toString(8));      // Octal
console.log(num.toString(16));     // Hexadecimal

// ---------- Parsing ----------

console.log(parseInt("100px"));      // 100
console.log(parseInt("12.9"));       // 12
console.log(parseFloat("12.9px"));   // 12.9
console.log(parseInt("101", 2));     // 5

// ---------- Math with Special Values ----------

console.log(1 / 0);     // Infinity
console.log(-1 / 0);    // -Infinity
console.log(0 / 0);     // NaN

// ---------- Floating Point Precision ----------

console.log(0.1 + 0.2); // 0.30000000000000004

// Correct comparison
console.log(Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON); // true
Number.MAX_VALUE

This is the largest finite number JavaScript can represent. If you exceed this value, you will get infinity.

Number.MIN_VALUE

This is not the most negative number. It’s the smallest positive number greater than 0.

which is completely different.

Number.MAX_SAFE_INTEGER
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991

JavaScript can accurately represent integers only up to this value.

let x = Number.MAX_SAFE_INTEGER;

console.log(x + 1); // 9007199254740992
console.log(x + 2);  // 9007199254740992

This is why BigInt was introduced.

Number.EPSILON

Because floating-point numbers are approximations, direct equality comparisons can fail.

console.log(0.1 + 0.2 === 0.3); // false

// do this instead 
Math.abs((0.1 + 0.2) - 0.3) < Number.EPSILON // This checks whether the difference is tiny enough to be considered equal.

Number.isNaN() vs Global isNaN()

console.log(isNaN("Hello"));          // true
console.log(Number.isNaN("Hello"));   // false
console.log(Number.isNaN(NaN));       // true

isNaN() first converts the value to a number, while Number.isNaN() checks only whether the value is actually NaN. Prefer Number.isNaN().

parseInt() vs Number()
console.log(Number("123abc"));   // NaN
console.log(parseInt("123abc")); // 123

Number() requires the entire string to be numeric. parseInt() reads until it encounters an invalid character.

Questions:

Q1. What is the difference between Number() and new Number()?

console.log(typeof Number(5));      // "number"
console.log(typeof new Number(5));  // "object"

Number() returns a primitive, while new Number() creates a wrapper object. Use Number(), not new Number().

Q2. Why does 0.1 + 0.2 !== 0.3?

Because JavaScript uses binary floating-point representation, and values like 0.1 and 0.2 cannot be represented exactly in binary.

Q3. Why is Number.MAX_SAFE_INTEGER important?

Beyond this value, integer arithmetic may become inaccurate because the floating-point format cannot represent every integer precisely.

Q4. What’s the difference between parseInt(), parseFloat(), and Number()?

console.log(parseInt("12.9px"));   // 12
console.log(parseFloat("12.9px")); // 12.9
console.log(Number("12.9px"));     // NaN

Q5. What is the difference between Number.isFinite() and global isFinite()?

console.log(isFinite("100"));          // true
console.log(Number.isFinite("100"));   // false

console.log(Number.isFinite(100));     // true

The global version coerces its argument to a number; Number.isFinite() does not.

Q6. Why does typeof NaN return "number"?

NaN stands for “Not-a-Number,” but it is a special value within the number type used to represent invalid numeric results.

console.log(typeof NaN); // "number"

The Math Object

The Math object is a built-in JavaScript object that provides mathematical constants and functions. Unlike Number, it is not a constructor, so you never write new Math().

console.log(Math.PI);          // 3.141592653589793
console.log(Math.E);           // 2.718281828459045
console.log(Math.LN2);         // Natural log of 2
console.log(Math.LN10);        // Natural log of 10
console.log(Math.LOG2E);       // log₂(e)
console.log(Math.LOG10E);      // log₁₀(e)
console.log(Math.SQRT2);       // √2
console.log(Math.SQRT1_2);     // 1/√2

// Rounding
console.log(Math.round(4.6));  // 5
console.log(Math.round(4.4));  // 4
console.log(Math.floor(4.9));  // 4
console.log(Math.ceil(4.1));   // 5
console.log(Math.trunc(4.9));  // 4
console.log(Math.trunc(-4.9)); // -4

// Absolute value
console.log(Math.abs(-10));    // 10

// Power & Roots
console.log(Math.pow(2, 5));   // 32
console.log(Math.sqrt(64));    // 8
console.log(Math.cbrt(27));    // 3

// Min & Max
console.log(Math.min(5, 2, 8, 1)); // 1
console.log(Math.max(5, 2, 8, 1)); // 8

// Random
console.log(Math.random()); // Between 0 (inclusive) and 1 (exclusive)

// Trigonometry
console.log(Math.sin(Math.PI / 2)); // 1
console.log(Math.cos(0));           // 1
console.log(Math.tan(Math.PI / 4)); // 1

// Logarithms
console.log(Math.log(Math.E)); // 1
console.log(Math.log10(100));  // 2
console.log(Math.log2(8));     // 3

// Sign
console.log(Math.sign(-5)); // -1
console.log(Math.sign(0));  // 0
console.log(Math.sign(5));  // 1

// Miscellaneous
console.log(Math.exp(1));      // e¹
console.log(Math.hypot(3, 4)); // 5
console.log(Math.imul(2, 4));  // 8
console.log(Math.fround(1.337));

General formula to generate random number between two values:

Math.floor(Math.random() * (max - min + 1)) + min;
Questions

What is the difference between Math.floor(), Math.ceil(), Math.round(), and Math.trunc()?

Method4.9-4.9
Math.floor()4-5
Math.ceil()5-4
Math.round()5-5
Math.trunc()4-4

Why does Math.min([1, 2, 3]) return NaN?

Because Math.min() expects individual arguments, not an array.

Math.min(1, 2, 3);      // 1
Math.min([1, 2, 3]);    // NaN
Math.min(...[1, 2, 3]); // 1

The spread operator converts the array into separate arguments.

6. Strings and the String Object

A string is a sequence of characters used to represent text. In JavaScript, strings are primitive values and are immutable, meaning once a string is created, its contents cannot be changed. JavaScript also provides a String wrapper object that exposes many useful methods.

// Creating strings
let str1 = "Hello";
let str2 = 'World';
let str3 = `Hello JavaScript`; // Template literal
let str4 = String(123);        // "123"
let str5 = new String("Hello"); // Wrapper object (rarely used)

console.log(typeof str1); // "string"
console.log(typeof str5); // "object"

// Length
console.log(str1.length); // 5

// Character Access
console.log(str1[0]);         // H
console.log(str1.charAt(1));  // e
console.log(str1.at(-1));     // o

// Searching
console.log(str1.indexOf("l"));       // 2
console.log(str1.lastIndexOf("l"));   // 3
console.log(str1.includes("ell"));    // true
console.log(str1.startsWith("He"));   // true
console.log(str1.endsWith("lo"));     // true

// Extracting
console.log(str1.slice(1, 4));        // ell
console.log(str1.substring(1, 4));    // ell

// Splitting
console.log("a,b,c".split(","));      // ["a", "b", "c"]

// Replacing
console.log("Hello World".replace("World", "JS"));
console.log("a-a-a".replaceAll("a", "b"));

// Case Conversion
console.log("hello".toUpperCase());
console.log("HELLO".toLowerCase());

// Removing Spaces
console.log("   Hello   ".trim());
console.log("   Hello".trimStart());
console.log("Hello   ".trimEnd());

// Padding
console.log("5".padStart(3, "0")); // 005
console.log("5".padEnd(3, "*"));   // 5**

// Repeating
console.log("Hi ".repeat(3));

// Concatenation
console.log("Hello".concat(" ", "World"));
console.log("Hello" + " World");

// Template Literals
let name = "John";
console.log(`Hello ${name}`);

// Comparison
console.log("apple" < "banana"); // true

// Conversion
console.log(String(true));
console.log((123).toString());

// Unicode
console.log("A".charCodeAt(0));     // 65
console.log(String.fromCharCode(65)); // A

Unlike normal strings, template literals support:

  • Variable interpolation
  • Multi-line strings

JavaScript temporarily wraps the primitive string in a String object behind the scenes, allowing access to properties and methods. This is called autoboxing.

7. Arrays

An array is an ordered collection of values. Unlike many programming languages, JavaScript arrays can store different data types together. Arrays are objects, are zero-indexed, and are dynamic, meaning they can grow or shrink at runtime.

let arr = [
    10,
    "Hello",
    true,
    null,
    { name: "John" },
    [1, 2, 3]
];

console.log(Array.isArray(arr)); // true
console.log(typeof arr);         // object

// Properties
console.log(arr.length);

// Access
console.log(arr[0]);
console.log(arr.at(-1));

// Add Elements
arr.push(100);     // Adds to the end.
arr.unshift("Start");  // Adds to the beginning.

// Remove Elements
arr.pop();         // Removes the last element
arr.shift();       // removes the first element

// Insert/Remove Anywhere
arr.splice(2, 1, "New Value");

// Copy Portion
console.log(arr.slice(1, 4));

// Search
console.log(arr.indexOf("Hello"));
console.log(arr.includes(true));
console.log(arr.find(v => typeof v === "object"));    // returns the first matching element.
console.log(arr.findIndex(v => typeof v === "boolean"));

// Transformation
console.log([1, 2, 3].map(x => x * 2));
console.log([1, 2, 3, 4].filter(x => x % 2 === 0));
console.log([1, 2, 3].reduce((sum, x) => sum + x, 0));

// Iteration
arr.forEach(item => console.log(item));

for (const value of arr) {
    console.log(value);
}

// Combining
console.log([1, 2].concat([3, 4]));
console.log([...arr]);

// Sorting
console.log([5, 2, 10].sort((a, b) => a - b));

// Reverse
console.log([1, 2, 3].reverse());

// Join
console.log(["a", "b", "c"].join("-"));

// Flat
console.log([1, [2, [3]]].flat(2)); // [1, 2, 3]

// Fill
console.log(new Array(5).fill(0)); // [0, 0, 0, 0, 0]

// From
console.log(Array.from("Hello")); // ["H", "e", "l", "l", "o"]

// Keys, Values, Entries
console.log([...arr.keys()]);    // [0, 1]
console.log([...arr.values()]);  // ["A", "B"]
console.log([...arr.entries()]); // [[0, "A"], [1, "B"]]
let numbers = new Array(1, 2, 3);  // avoid this unless needed

console.log([5]); // [5]
console.log(new Array(5)); // [ <5 empty items> ]

// The constructor behaves differently when given a single numeric argument.

splice() It can insert delete and replace:

array.splice(start, deleteCount, ...items)
let arr = [1, 2, 3, 4];

arr.splice(1, 2, 100, 200);

console.log(arr);  // [1, 100, 200, 4]

slice()Returns a copy without modifying the original.

forEach()

Purpose

Execute some code for every element in an array.

Syntax
array.forEach(function(currentValue, index, array) {
    // code
}, thisArg);

The callback receives three arguments. You can name them anything. The names don’t matter only the position does. forEach() always returns undefined. It does not create a new array.

const fruits = ["Apple", "Banana", "Mango"];

fruits.forEach(function(value, index, array) {
    console.log(value);
    console.log(index);
    console.log(array);
});
Apple
0
["Apple","Banana","Mango"]

Banana
1
["Apple","Banana","Mango"]

Mango
2
["Apple","Banana","Mango"]

map()

Transform every element into something else.

Input Array → Transformation → New Array

Syntax
array.map(function(currentValue, index, array) {
    return something;
}, thisArg);

Notice the callback arguments are exactly the same as forEach().

const numbers = [1,2,3,4];

const doubled = numbers.map(function(num) {
    return num * 2;
});

console.log(doubled);
[2,4,6,8]
[1,2,3,4]

↓

×2

↓

[2,4,6,8]

Original array

console.log(numbers); // [1,2,3,4]

The original array is not modified.

const arr = [10,20,30];

const result = arr.map(function(value, index, array) {
    console.log(value);
    console.log(index);
    console.log(array);

    return value + index;
});

console.log(result); // [10,21,32]

Mistake:

const result = [1,2,3].map(num => {
    num * 2;
});

console.log(result); // [undefined, undefined, undefined] because you have to return it 

filter()

Keep only elements that satisfy a condition.

Syntax
array.filter(function(currentValue, index, array) {
    return true or false;
});
const numbers = [1,2,3,4,5,6];

const even = numbers.filter(num => num % 2 === 0);

console.log(even); // [2,4,6]

reduce()

This is the method most developers initially find difficult. Convert an entire array into one value.

Syntax
array.reduce(function(accumulator, currentValue, index, array){

}, initialValue);
ArgumentMeaning
accumulatorResult built so far
currentValueCurrent element
indexCurrent index
arrayOriginal array
const numbers = [1,2,3,4];

const sum = numbers.reduce(function(acc, current){
    return acc + current;
},0);

console.log(sum); // 10
const sum = [1,2,3].reduce((acc,current)=>{
    return acc + current;
}); 

// without inital value acc = first element current = second element

sort()

Arrange elements. sort() mutates the original array

Default Behavior
const nums = [100,5,20];

console.log(nums.sort()); // [100,20,5]

Because JavaScript compares them as strings.

"100"

"20"

"5"
Comparator Function
array.sort(function(a,b){

});

Only two callback arguments.

ArgumentMeaning
aFirst element
bSecond element
const nums=[100,5,20];

nums.sort((a,b)=>a-b);

console.log(nums); // [5,20,100] asc ordr 

nums.sort((a,b)=>b-a); // desc [100,20,5]

some()

Returns true if at least one element satisfies the condition.

const nums=[1,3,5,8];

console.log(nums.some(n=>n%2===0)); // true

every()

Returns true only if every element satisfies the condition.

const nums=[2,4,6];

console.log(nums.every(n=>n%2===0)); // true

Some extra array methods

// Sample array
const numbers = [1, 2, 3, 4, 5];
const nested = [[1], [2, 3], [4, [5]]];

// ==========================
// flat()
// ==========================

console.log(nested.flat());        // [1, 2, 3, 4, [5]]
console.log(nested.flat(2));       // [1, 2, 3, 4, 5]

// ==========================
// flatMap()
// map() then flat(1)
// ==========================

const words = ["Hello World", "JavaScript Arrays"];

console.log(
    words.flatMap(word => word.split(" "))
);
// ["Hello", "World", "JavaScript", "Arrays"]

// ==========================
// reduceRight()
// Processes from right to left
// ==========================

console.log(
    ["A", "B", "C"].reduceRight((acc, curr) => acc + curr)
);
// "CBA"

// ==========================
// copyWithin()
// Copies elements inside the same array
// Mutates original array
// ==========================

const arr1 = [1, 2, 3, 4, 5];

arr1.copyWithin(0, 3);

console.log(arr1);
// [4, 5, 3, 4, 5]

// ==========================
// fill()
// Mutates original array
// ==========================

const arr2 = new Array(5);

arr2.fill(10);

console.log(arr2);
// [10, 10, 10, 10, 10]

const arr3 = [1,2,3,4,5];

arr3.fill(0,2,4);

console.log(arr3);
// [1,2,0,0,5]

// ==========================
// Array.from()
// ==========================

console.log(Array.from("Hello"));
// ["H","e","l","l","o"]

console.log(Array.from({length:5}, (_, i) => i + 1));
// [1,2,3,4,5]

// ==========================
// Array.of()
// ==========================

console.log(Array.of(5));
// [5]

console.log(new Array(5));
// [ <5 empty items> ]


// ==========================
// toSorted()
// Doesn't mutate original
// ==========================

const nums1 = [5,2,8,1];

const sorted = nums1.toSorted((a,b)=>a-b);

console.log(sorted);
// [1,2,5,8]

console.log(nums1);
// [5,2,8,1]

// ==========================
// toReversed()
// Doesn't mutate original
// ==========================

const nums2 = [1,2,3];

const reversed = nums2.toReversed();

console.log(reversed);
// [3,2,1]

console.log(nums2);
// [1,2,3]

// ==========================
// toSpliced()
// Doesn't mutate original
// ==========================

const nums3 = [1,2,3,4];

const updated = nums3.toSpliced(1,2,100,200);

console.log(updated);
// [1,100,200,4]

console.log(nums3);
// [1,2,3,4]

// ==========================
// with()
// Replace one element
// Doesn't mutate original
// ==========================

const nums4 = [10,20,30];

const changed = nums4.with(1,999);

console.log(changed);
// [10,999,30]

console.log(nums4);
// [10,20,30]

// ==========================
// at()
// Supports negative indexing
// ==========================

console.log(numbers.at(-1));
// 5

console.log(numbers.at(-2));
// 4

When would you use entries() instead of forEach()?

Use entries() with a for...of loop when you need both the index and the value and want the flexibility of loop control (break, continue), which forEach() doesn’t support.

8. Objects

Objects are the foundation of JavaScript. Almost everything you use arrays, functions, dates, promises, maps, sets, regexes is an object or behaves like one. An object is a collection of key-value pairs.

// ============================
// Creating Objects
// ============================

const person = {
    firstName: "John",
    lastName: "Doe",
    age: 25,
    isStudent: false,

    hobbies: ["Reading", "Gaming"],

    address: {
        city: "Delhi",
        country: "India"
    },

    greet() {
        return `Hello ${this.firstName}`;
    }
};

// ============================
// Accessing Properties
// ============================

console.log(person.firstName);
console.log(person["lastName"]);

const key = "age";
console.log(person[key]);

// ============================
// Access Nested Object
// ============================

console.log(person.address.city);

// ============================
// Access Array
// ============================

console.log(person.hobbies[0]);

// ============================
// Calling Methods
// ============================

console.log(person.greet());

// ============================
// Add Properties
// ============================

person.email = "john@example.com";

// ============================
// Update Properties
// ============================

person.age = 26;

// ============================
// Delete Properties
// ============================

delete person.isStudent;

// ============================
// Property Existence
// ============================

console.log("age" in person);
console.log("salary" in person);

// ============================
// Optional Chaining
// ============================

console.log(person.company?.name);

// ============================
// Nullish Coalescing
// ============================

console.log(person.salary ?? "Not Available");

// ============================
// Object References
// ============================

const user1 = {
    name: "Alice"
};

const user2 = user1;

user2.name = "Bob";

console.log(user1.name);
console.log(user2.name);

// ============================
// Shallow Copy
// ============================

const copy = {
    ...person
};

copy.firstName = "Jane";

console.log(person.firstName); // Jane
console.log(copy.firstName);   // Jane

// ============================
// Nested Object Problem
// ============================

const employee = {
    name: "Alex",
    address: {
        city: "Mumbai"
    }
};

const employeeCopy = {
    ...employee
};

employeeCopy.address.city = "Pune";

console.log(employee.address.city);  // Pune
console.log(employeeCopy.address.city); // Pune

When property names contain spaces use brackets instead of dot notation to access them. Objects can contain every JavaScript data type, including other objects and functions. Functions inside objects are called methods.

const person = {

    name:"John",

    greet(){

        console.log("Hello");
    }
};

person.greet();
const person = {};

person.name = "John";

person.age = 25;

console.log(person.salary)  // undefined
console.log(person.salary === undefined)  // true
console.log("salary" in person); // false
const user1 = {
    name: "John"
};

const user2 = user1;

user2.name = "Alex";

console.log(user1.name); // Alex because objects are defined by reference
// Shallow Copy

const person = {};

person.name = "John";

person.age = 25;

const copy = {
    ...person
};

console.log(copy.name); // John 

copy.name = "Alex";

console.log(copy.name); // Alex
console.log(person.name); // John


// spread operator performs a shallow copy, not a deep copy.
const person = {
    address: {
        city: "Delhi"
    }
};

const copy = {
    ...person
};

copy.address.city = "Mumbai";

console.log(person.address.city); // Mumbai because internal object is not changing

The last property with the same name overwrites the previous one.

const key = "age";

const person = {
    [key]: 25
};

console.log(person);

// This is called a computed property nam
const a = {};
const b = {};

console.log(a === b); // false

// Even though both objects are empty, they are different objects in memory.

One of the first things to understand is that Object (capital “O”) is itself a built-in constructor function. It provides many static methods that operate on objects.

const person = {
    name: "John",
    age: 25,
};

// ============================
// Object.keys()
// ============================

console.log(Object.keys(person)); // ["name", "age"]

// ============================
// Object.values()
// ============================

console.log(Object.values(person)); // ["John", 25]

// ============================
// Object.entries()
// ============================

console.log(Object.entries(person)); // [["name", "John"],["age", 25]]

// ============================
// Object.fromEntries()
// ============================

const entries = [
    ["name", "Alex"],
    ["age", 30]
];

console.log(Object.fromEntries(entries)); // outputs the new object

// ============================
// Object.assign()
// ============================

// You can also merge objects using assign
const copy = Object.assign({}, person); // Copies properties from one or more source objects into a target object.

copy.name = "Jane";

console.log(copy);
console.log(person);

// ============================
// Object.freeze()
// ============================

// Makes an object completely immutable. Can't add or delete. freeze() is shallow.
const frozen = Object.freeze({
    x: 10
});

frozen.x = 20;
delete frozen.x;

console.log(frozen);

// ============================
// Object.seal()
// ============================

// Allows modifying existing properties but prevents adding or deleting properties.
const sealed = Object.seal({
    a: 1
});

sealed.a = 100;
sealed.b = 200;
delete sealed.a;

console.log(sealed);

// ============================
// Object.hasOwn() - Checks whether an object has its own property.
// ============================

console.log(
    Object.hasOwn(person, "name")
);

console.log(
    Object.hasOwn(person, "salary")
);

// ============================
// Object.is()
// ============================

console.log(
    NaN === NaN
); // false
console.log(Object.is(NaN, NaN)); // true
console.log(+0 === -0); // true
console.log(Object.is(+0, -0)); // false 
console.log(Object.is(10, 10));

// ============================
// Object.create() - Creates an object with a specified prototype.
// ============================

const animal = {
    eats: true
};

const dog = Object.create(animal);

dog.name = "Tommy";

console.log(dog.eats);

// ============================
// Object.defineProperty() - Defines a property with custom behavior.
// ============================

const user = {};

Object.defineProperty(user, "id", {
    value: 101,    // value
    writable: false,  // Can it be changed?
    enumerable: true,   // Appears in loops and Object.keys()?
    configurable: false  // Can it be deleted or reconfigured?
});

console.log(user.id);

// user.id = 200; // ignored

// ============================
// Object.getOwnPropertyDescriptor() - Shows the descriptor of a single property.
// ============================

console.log(
    Object.getOwnPropertyDescriptor(user, "id")
);

// ============================
// Object.getOwnPropertyDescriptors() - Returns descriptors for all properties.
// ============================

console.log(
    Object.getOwnPropertyDescriptors(person)
);

ES6 (ECMAScript 2015) introduced several features that made working with objects much cleaner and more expressive. Today, these features are used everywhere in React, Node.js, Express, Next.js, and modern JavaScript applications.

const firstName = "John";
const lastName = "Doe";
const age = 25;

// ============================
// Property Shorthand
// ============================

const person = {
    firstName,
    lastName,
    age,

    // Method Shorthand
    greet() {
        return `Hello ${this.firstName}`;
    }
};

console.log(person);

// ============================
// Computed Property Names
// ============================

const key = "country";

const user = {
    [key]: "India",
    ["favorite" + "Color"]: "Blue"
};

console.log(user);

// ============================
// Destructuring
// ============================

const {
    firstName: fName,
    age: personAge
} = person;

console.log(fName);
console.log(personAge);

// ============================
// Default Values
// ============================

const {
    salary = 50000
} = person;

console.log(salary); // The default is only used when the property is undefined.

// ============================
// Nested Destructuring
// ============================

const employee = {
    name: "Alex",

    address: {
        city: "Delhi",
        country: "India"
    }
};

const {
    address: {
        city,
        country
    }
} = employee;

console.log(city);
console.log(country);

// ============================
// Rest Properties
// ============================

const {
    firstName: name,
    ...remaining
} = person;

console.log(name);
console.log(remaining);

// ============================
// Spread Operator
// ============================

const updated = {
    ...person,
    age: 30,
    city: "Mumbai"
};

console.log(updated);

// ============================
// Merge Objects
// ============================

const obj1 = {
    x: 10
};

const obj2 = {
    y: 20
};

const merged = {
    ...obj1,
    ...obj2
};

console.log(merged);

// ============================
// Dynamic Keys
// ============================

const prop = "email";

const customer = {
    [prop]: "john@example.com"
};

console.log(customer);

// ============================
// Object Iteration
// ============================

for (const key in person) {
    console.log(key, person[key]);
}

Object.keys(person).forEach(key => {
    console.log(key);
});

Object.values(person).forEach(value => {
    console.log(value);
});

Object.entries(person).forEach(([key, value]) => {
    console.log(key, value);
});

Rest vs Spread: Although both use ..., they serve different purposes.

SpreadRest
Expands valuesCollects values
Used when creating objects or arraysUsed during destructuring or in function parameters

A getter looks like a property but actually runs a function.

const person = {

    firstName: "John",
    lastName: "Doe",

    get fullName() {
        return `${this.firstName} ${this.lastName}`;
    }
};

console.log(person.fullName);

There are no parentheses. It behaves like a property even though a function executes behind the scenes. Simillarly a setter runs automatically when you assign a value.

const person = {

    firstName: "",
    lastName: "",

    set fullName(value) {

        const parts = value.split(" ");

        this.firstName = parts[0];
        this.lastName = parts[1];
    }
};

person.fullName = "John Smith";

console.log(person.firstName);
console.log(person.lastName);

Prototype & Prototype Chain

A prototype is simply another object that an object can inherit properties and methods from.

const animal = {
    eats: true
};

const dog = Object.create(animal);

dog.name = "Tommy";

console.log(dog.name); // Tommy
console.log(dog.eats); // true

Although dog doesn’t have an eats property, it inherits it from its prototype. This search process is called the prototype chain. Every object has a prototype.

const person = {};
Object.getPrototypeOf(person); // Object.prototype
// Many properties you see automatically on objects like toString() etc are all coming from Object.prototype
const obj = Object.create(null);
console.log(Object.getPrototypeOf(obj)); // null 

These objects:

  • have no inherited methods,
  • don’t inherit toString(),
  • don’t inherit hasOwnProperty().

They’re often used as pure key-value dictionaries to avoid accidental collisions with inherited property names.

__proto__ vs prototype

These two are commonly confused.

__proto__

Exists on objects.

const obj = {};

console.log(obj.__proto__);

It refers to the object’s prototype. It’s mainly for legacy compatibility; prefer Object.getPrototypeOf() and Object.setPrototypeOf().

prototype

Exists on functions.

function Person() {}

console.log(Person.prototype);

This is the object that becomes the prototype of instances created with new Person().

Constructor Functions & the new Keyword

Before ES6 classes existed, JavaScript developers used constructor functions to create multiple objects of the same type. Even today, classes are built on top of this mechanism. A constructor is just a normal function that’s intended to be called with new.

function Person(name, age) {
    this.name = name;
    this.age = age;
}

const p1 = new Person("John", 25);
const p2 = new Person("Jane", 30);

console.log(p1);
console.log(p2);

The this refers to the new object being created. Calling new does following things:

const obj = {}; // 1. Create a New Empty Object

Object.setPrototypeOf(obj, Person.prototype); // 2. Link the Prototype

Person.call(obj, "John", 25); // 3. Call the Constructor

return obj; // 4. Return the Object

Here is the full example:

// ========================
// Constructor Function
// ========================

function Person(name, age) {
    this.name = name;
    this.age = age;
}

// ========================
// Prototype Method
// ========================

Person.prototype.greet = function () {
    console.log(`Hello ${this.name}`);
};

// ========================
// Create Objects
// ========================

const p1 = new Person("John", 25);
const p2 = new Person("Jane", 30);

console.log(p1); // Person { name: 'John', age: 25 }
console.log(p2); // Person { name: 'Jane', age: 30 }

// ========================
// Shared Method
// ========================

p1.greet(); // Hello John
p2.greet(); // Hello Jane

// ========================
// Prototype Check
// ========================

console.log(
    Object.getPrototypeOf(p1) === Person.prototype 
); // true

// ========================
// instanceof
// ========================

console.log(p1 instanceof Person); // true

// ========================
// constructor
// ========================

console.log(p1.constructor); // [Function: Person]

// ========================
// Own Properties
// ========================

console.log(Object.keys(p1)); // [ 'name', 'age' ]

// ========================
// Inherited Method
// ========================

console.log(Object.hasOwn(p1, "greet")); // false (because 'greet' is on the prototype, not the instance itself)

console.log("greet" in p1); // true (because 'in' checks the whole prototype chain)

// ========================
// Calling Without new
// ========================

// Person("Alex", 22); // If uncommented, this returns `undefined` and (in non-strict mode) pollutes the global object by setting window.name="Alex" and window.age=22.

We put methods on the prototype because if we define methods inside the function then every object gets its own copy of the function. That’s wasteful. This is one of the main reasons JavaScript uses prototypes.

What Happens Without new?

function Person(name) {
    this.name = name;
}

Person("John");

Here, JavaScript treats Person as a normal function call.

  • In non-strict mode, this becomes the global object (such as window in browsers), which can accidentally create global properties.
  • In strict mode, this is undefined, so this.name = ... throws a TypeError.

That’s why constructor functions are meant to be called with new.

1. prototype (Blueprint)

  • Who has it? Only functions (specifically constructor functions and classes).
  • What is it? It is an object attached to a function that acts as a template.
  • Purpose: You put methods and properties here that you want all instances of this constructor to share, saving memory.
function Person(name) {
    this.name = name;
}

// Adding a shared method to the blueprint
Person.prototype.sayHi = function() {
    console.log("Hi!");
};

2. __proto__ (The Link)

  • Who has it? Every object in JavaScript (including functions, arrays, and plain objects).
  • What is it? It is a hidden reference (pointer) that links a specific object instance back to the prototype it was created from.
  • Purpose: It is the actual mechanism JavaScript uses to look up the “prototype chain.” If an object doesn’t have a property, the engine looks at the object’s __proto__ to find it.
const p1 = new Person("John");

// The engine uses __proto__ behind the scenes to find sayHi
p1.sayHi();

When you create a new object using the new keyword, JavaScript automatically sets the object’s __proto__ to point to the constructor function’s prototype.

// This is ALWAYS true for instances
console.log(p1.__proto__ === Person.prototype); // true

Prototypal inheritance is JavaScript’s unique way of sharing properties and methods between objects. Here is how the mechanism works:

1. The Prototype Chain Lookup

When you try to access a property or method on an object, the JavaScript engine follows a specific sequence:

  1. Check the object itself: Does this exact object have the property? If yes, use it.
  2. Check the __proto__: If not, look at the object’s hidden __proto__ link. Does the parent object have it?
  3. Climb the chain: If the parent doesn’t have it, check the parent’s __proto__.
  4. End of the line: This continues until the engine reaches null (usually the top of the chain, Object.prototype). If it hits null and still hasn’t found the property, it returns undefined.

The cleanest way to see pure prototypal inheritance in action is using Object.create(). This method creates a brand new object and explicitly sets its __proto__ to whatever object you pass in. Before modern syntax, developers used constructor functions to link prototypes together.

function Person(name) {
    this.name = name;
}
Person.prototype.greet = function() {
    console.log(`Hello, I'm ${this.name}`);
};

function Developer(name, language) {
    // Call the parent constructor, passing the current context ('this')
    Person.call(this, name); 
    this.language = language;
}

// Link Developer's prototype to Person's prototype
Developer.prototype = Object.create(Person.prototype);
// Fix the constructor pointer that was overwritten
Developer.prototype.constructor = Developer; 

Developer.prototype.code = function() {
    console.log(`${this.name} is writing ${this.language}`);
};

const dev1 = new Developer("Alice", "JavaScript");
dev1.greet(); // Hello, I'm Alice (Found on Person.prototype)
dev1.code();  // Alice is writing JavaScript (Found on Developer.prototype)

Today, we almost exclusively use the class and extends keywords. It looks exactly like classical inheritance from Java or C++, but under the hood, it is still doing exactly what is shown before. It is just syntactic sugar over the prototype chain.

Under the hood, functions have special hidden internal properties that regular objects don’t have. [[Call]] and [[Construct]].

  • [[Call]]: This allows the object to be executed using parentheses ().
  • [[Construct]]: This allows the object to be invoked with the new keyword to stamp out new objects.

Because plain objects ({}) don’t have these hidden properties, you can’t run them like myObject() or do new myObject().

Some Clarity:

1. Object (The Built-in Function)

When you see Object.prototype, you are looking at the blueprint attached to the global Object function. JavaScript provides this function out-of-the-box so you can create plain objects.

// Object with a capital 'O' is a function!
console.log(typeof Object); // "function"

// Because it's a function, it has a .prototype property
console.log(Object.prototype); // [Object: null prototype] {}

Note: Object.prototype is the “master blueprint”. It holds the default methods that every single object in JavaScript gets to use, like .toString() and .hasOwnProperty().

2. {} (The Object Instance)

When you create a plain object using {}, you are actually calling that Object function behind the scenes to stamp out a new instance.

Just like your p1 instance from earlier didn’t have a .prototype, your plain object instance doesn’t have one either. Instead, it gets a __proto__ link that points back to the master blueprint.

const myObj = {}; // This is an instance

// It is NOT a function, so it has no .prototype property
console.log(myObj.prototype); // undefined

// BUT it has a __proto__ link pointing back to the function that built it
console.log(myObj.__proto__ === Object.prototype); // true

ES6 classes

ES6 classes were introduced to JavaScript to make object-oriented programming feel more familiar to developers coming from languages like Java or Python.

However, the most important thing to know about ES6 classes is that they are “syntactic sugar”. Under the hood, JavaScript does not have real classes. It is still using the exact same functions, .prototype, and __proto__ mechanisms we just explored. It just hides the messy wiring for you.

1. The Basic Class (Constructors & Prototypes)

When you write a class, you are actually writing a constructor function and attaching methods to its prototype, all in one clean block.

class Person {
    // This is the actual Constructor Function
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }

    // This automatically goes to Person.prototype
    greet() {
        console.log(`Hello, I'm ${this.name}`);
    }
}

const p1 = new Person("John", 25);

What JavaScript is doing behind the scenes:

function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.greet = function() {
    console.log(`Hello, I'm ${this.name}`);
};

2. Inheritance (extends and super)

This is where ES6 classes shine. Setting up the prototype chain manually with Object.create and resetting constructor pointers (as we did earlier) was error-prone. ES6 gives us two keywords to handle it automatically:

  • extends: Automatically links the child’s .prototype to the parent’s .prototype behind the scenes.
  • super(): Calls the parent’s constructor function so the parent can set up its properties on the new object.

If you create a class with extends and write a constructor, you must call super() before you use the this keyword. If you don’t, JavaScript will throw a fatal ReferenceError. In a normal class, the constructor automatically creates a new empty object and assigns it to this. However, in a derived class (one that uses extends), the engine says: “I am not going to build the this object. I am going to let the parent class build it.” super() is the command that tells the parent class to do its job and hand the built object back down to the child. If you don’t call super(), this literally does not exist yet! If your child class doesn’t have a constructor at all, JavaScript automatically creates a hidden one that calls super(...args) for you. If you saw a child class that didn’t have super, it is almost certainly because it didn’t have a constructor at all.

class Developer extends Person {
    constructor(name, age, language) {
        // super() calls the Person constructor! 
        // It's the ES6 equivalent of Person.call(this, name, age)
        super(name, age); 
        this.language = language;
    }

    // Goes to Developer.prototype
    code() {
        console.log(`${this.name} writes ${this.language}`);
    }
}

const dev1 = new Developer("Jane", 30, "JavaScript");
dev1.greet(); // Works! Found on Person.prototype via __proto__
dev1.code();  // Works! Found on Developer.prototype
// 1. A class is literally just a function
console.log(typeof Person); // "function"

// 2. The methods are on the prototype object
console.log(Object.getOwnPropertyNames(Person.prototype)); 
// ['constructor', 'greet']

// 3. The instance is linked via __proto__
console.log(dev1.__proto__ === Developer.prototype); // true

// 4. The prototype chain is linked exactly the same way
console.log(Developer.prototype.__proto__ === Person.prototype); // true

If you try to call a class without the new keyword (e.g., Person()), JavaScript will throw an error. With old constructor functions, doing that would accidentally pollute the global object.

Public Fields: Declares properties directly on the instance without needing to put them inside the constructor. Private Fields#password = "123";Hides properties or methods. They cannot be read or changed from outside the class. Static Membersstatic count = 0;Attaches properties/methods to the class blueprint itself, not to the individual instances. Getters / Setters: Acts like a property, but runs a function behind the scenes. Great for formatting or validating data.

If a child class has a method with the exact same name as a parent class, it overrides the parent’s method. However, you can still call the parent’s version from inside the child using super.methodName().

Sometimes you need to do complex logic to set up your static variables. JavaScript allows you to write a static {} block that runs exactly once when the class is first loaded into memory.

class Database {
    static connectionString;

    // This block runs once as soon as JavaScript reads the class
    static {
        const env = "production";
        if (env === "production") {
            this.connectionString = "prod-db-url";
        } else {
            this.connectionString = "dev-db-url";
        }
    }
}

Remmeber classes are not hoisted unlike functions. Just like you can assign a function to a variable, you can assign an entire class to a variable.

Shallow vs Deep Copy

Because variables only hold pointers to objects, copying objects gets complicated. If you just do let newObj = oldObj, you aren’t copying the object; you are just copying the pointer. Both variables now point to the exact same warehouse address. If you change one, the other changes.

To actually copy the data, we have to create clones.

Shallow Copy (The Surface Clone)

A shallow copy creates a brand new object in the Heap for the top layer, but it stops there. If your object has other objects nested inside it, the shallow copy just copies the pointers to those inner objects.

  • How to do it: Spread operator ({ ...obj }), Object.assign(), or .slice() for arrays.
  • The Danger: The top level is safe to modify, but if you change a nested object, the original object will change too, because they share the same inner pointer.

Deep Copy (The Total Clone)

A deep copy creates a brand new object, looks inside it, finds any nested objects, creates brand new copies of those, and so on. It duplicates the entire tree. The new object shares absolutely zero memory addresses with the original.

  • How to do it: Modern JavaScript has structuredClone(obj). The older hack is JSON.parse(JSON.stringify(obj)).
  • The Benefit: You can modify the clone completely safely without ever affecting the original.

9. This Keyword

Think of this as a pronoun in English. If I say “My car is fast,” the word “My” changes meaning depending on who is speaking. this works the exact same way. It’s a placeholder that JavaScript swaps out at the exact moment a function runs. To master this, you only need to know the Four Rules of Invocation, plus one special exception.

1. Implicit Binding (The “Dot” Rule)

This is the most common way this is used. When a function is called as a method of an object, this refers to the object standing to the left of the dot when the function is called.

const user = {
    name: "Alice",
    greet() {
        console.log(`Hello, I'm ${this.name}`);
    }
};

// "user" is to the left of the dot, so this = user
user.greet(); // "Hello, I'm Alice"

2. Explicit Binding (The call, apply, bind Rule)

Sometimes you want to force this to be exactly what you want, regardless of how it’s called. JavaScript gives you three methods .call(), .apply(), and .bind()that exist on every function specifically for this purpose.

const user1 = { name: "Alice" };
const user2 = { name: "Bob" };

function greet() {
    console.log(`Hello, I'm ${this.name}`);
}

// We explicitly tell the function what "this" should be
greet.call(user1); // "Hello, I'm Alice"
greet.call(user2); // "Hello, I'm Bob"

The call method invokes a function immediately, setting its this context to the first argument provided. Any additional parameters must be passed in one by one as a comma-separated list. The apply method works exactly like call and executes the function immediately. The only difference is that it accepts additional arguments bundled together in a single array or array-like object. Unlike call and apply, the bind method does not execute the function right away. Instead, it creates and returns a brand new function copy with the this value permanently locked to the object you provided. You can run this new function whenever you want in the future.

3. New Binding (The new Keyword Rule)

As we saw when looking at classes and constructor functions, calling a function with the new keyword completely changes how the function behaves. When you use new, JavaScript automatically creates a brand new, empty object and forces this to point to that new object.

function Car(make) {
    // "new" creates an empty {} and points 'this' to it
    this.make = make; 
}

const myCar = new Car("Toyota");
console.log(myCar.make); // "Toyota"

4. Default Binding (The Global/Standalone Rule)

What happens if you just call a normal function on its own, with no dot, no call(), and no new?

function sayHi() {
    console.log(this);
}

sayHi(); // How is this called? As a standalone function.
  • In Non-Strict Mode: this defaults to the massive global window object (window in browsers, global in Node.js).
  • In Strict Mode ("use strict"): JavaScript decides that defaulting to the global object is a massive security/bug risk, so it sets this to undefined.

The Exception: Arrow Functions (=>)

The four rules above apply to normal functions declared with the function keyword. Arrow functions break all the rules. An arrow function does not have its own this. Instead, it uses “lexical scoping”. It looks up at the surrounding code and permanently adopts the this value of whatever function or scope it was written inside.

const timer = {
    name: "MyTimer",
    start() {
        setTimeout(function() {
            // BUG! setTimeout calls this function standalone (Rule 4)
            // 'this' is lost and becomes the global object / undefined
            console.log(`${this.name} finished!`); 
        }, 1000);
    }
};
timer.start(); // "undefined finished!"
const timer = {
    name: "MyTimer",
    start() {
        setTimeout(() => {
            // SUCCESS! Arrow functions don't care how they are called.
            // They inherit 'this' from the start() method (which is the timer object)
            console.log(`${this.name} finished!`); 
        }, 1000);
    }
};
timer.start(); // "MyTimer finished!"
const myButton = document.getElementById("submit-btn");

myButton.addEventListener("click", (event) => {
    // 'this' looks outside the function and finds the global scope
    console.log(this); // Window object
    
    // This will cause an error or silently fail:
    // this.style.backgroundColor = "red"; 
    
    // FIX: When using arrow functions, rely on the event object instead!
    event.currentTarget.style.backgroundColor = "red"; 
});
class Counter {
    constructor() {
        this.count = 0;
        this.btn = document.getElementById("increment-btn");
        
        // Passing a standard method reference
        this.btn.addEventListener("click", this.handleClick);
    }

    handleClick() {
        // ERROR! The browser forced 'this' to be the HTML <button>.
        // The button doesn't have a 'count' property, so this is NaN.
        this.count++; 
        console.log(this.count); 
    }
}

This is how you can fix this issue:

class Counter {
    constructor() {
        this.count = 0;
        this.btn = document.getElementById("increment-btn");
        
        // Wrapping it in an arrow function protects 'this'
        this.btn.addEventListener("click", () => this.handleClick());
    }

    handleClick() {
        // SUCCESS! 'this' remains the Counter instance.
        this.count++; 
        console.log(this.count); // 1, 2, 3...
    }
}

// Note: Another common way to fix this in classes is using .bind(this) in the constructor: this.handleClick = this.handleClick.bind(this);

10. Asynchronous JavaScript

JavaScript is strictly single-threaded. It has exactly one Call Stack. It can only execute one line of code at a time. If you tell JavaScript to fetch a 50MB image from a slow server, it should theoretically freeze the entire browser until the image loads, because it can’t move to the next line of code. But it doesn’t freeze. Why? Because of the Event Loop Architecture.

1. The Event Loop Architecture

When you run JavaScript in a browser (or Node.js), you aren’t just running the V8 JavaScript engine. You are using a whole environment built of four main pieces:

  1. The Call Stack: The single thread where your JS code actually runs. It works on a “Last In, First Out” (LIFO) basis.
  2. Web APIs (or C++ APIs in Node): Extra superpowers provided by the browser that JavaScript doesn’t natively have. This includes things like setTimeout, DOM events (onClick), and network requests (fetch).
  3. The Microtask Queue: A VIP waiting line for Promises.
  4. The Macrotask Queue (Callback Queue): A general waiting line for everything else (setTimeout, click events).

How it works (The Restaurant Analogy)

  • The Call Stack is a Waiter. There is only one Waiter.
  • Web APIs are the Kitchen.
  • When you call setTimeout or fetch, the Waiter takes the order, hands it to the Kitchen (Web APIs), and immediately goes back to serving the next line of code.
  • When the Kitchen finishes the task (the timer finishes, or the data downloads), it doesn’t shout at the Waiter. It puts the finished meal on a tray in the Queue.
  • The Event Loop is a bouncer that constantly checks: “Is the Call Stack (Waiter) completely empty?” If yes, it takes the first item from the Queue and pushes it onto the Call Stack to run.

JavaScript doesn’t treat all asynchronous callbacks equally. It gives extreme priority to Promises.

  • Microtask Queue (VIP Line): .then(), .catch(), .finally(), queueMicrotask().
  • Macrotask Queue (General Admission): setTimeout(), setInterval(), DOM Events.

The Event Loop rule is: The Microtask Queue must be completely emptied before it will even look at the Macrotask Queue.

Look at this code. What order do the numbers print in?

console.log(1);

setTimeout(() => {
    console.log(2);
}, 0);

Promise.resolve().then(() => {
    console.log(3);
});

console.log(4);

The Output: 1, 4, 3, 2

Why?

  1. console.log(1) goes to Call Stack. Prints 1.
  2. setTimeout goes to Web APIs. The timer is 0, so it instantly moves to the Macrotask Queue.
  3. Promise.resolve() resolves instantly. Its .then() callback goes to the Microtask Queue.
  4. console.log(4) goes to Call Stack. Prints 4.
  5. Call Stack is now empty. The Event Loop checks the VIP line (Microtask Queue). It finds the Promise. Prints 3.
  6. VIP line is empty. The Event Loop checks the Macrotask Queue. It finds the timeout. Prints 2.

Originally, we just passed functions into other functions to run when the task was done. This led to Callback Hell (or the Pyramid of Doom), where code grew horizontally and error handling became a nightmare.

// Callback Hell
getUser(userId, function(user) {
    getProfile(user.name, function(profile) {
        getPosts(profile.id, function(posts) {
            console.log("Finally got the posts!", posts);
        });
    });
});

A Promise is an object representing the eventual completion (or failure) of an asynchronous operation. It has three states: Pending, Fulfilled, or Rejected. Promises flattened the pyramid by allowing us to chain .then() and catch all errors in one place with .catch().

// The Promise Chain
getUser(userId)
    .then(user => getProfile(user.name))
    .then(profile => getPosts(profile.id))
    .then(posts => console.log("Got the posts!", posts))
    .catch(error => console.error("Something failed:", error));

async/await is syntactic sugar on top of Promises. It allows you to write asynchronous code that looks and reads like synchronous code. Under the hood, it is still using Promises and the Microtask Queue.

  • You must mark the function with async.
  • You use await in front of any Promise. This tells JavaScript: “Pause the execution of this specific function here until the Promise resolves. Go do other things on the main Call Stack in the meantime.”
// The Modern Way
async function fetchUserData(userId) {
    try {
        const user = await getUser(userId);
        const profile = await getProfile(user.name);
        const posts = await getPosts(profile.id);
        
        console.log("Got the posts!", posts);
    } catch (error) {
        // Errors are caught cleanly just like synchronous code
        console.error("Something failed:", error);
    }
}

Sometimes you have multiple API calls and you want to run them at the same time in parallel, rather than waiting for one to finish before starting the next. JavaScript provides four built-in methods for this:

  1. Promise.all([p1, p2]): Runs all promises in parallel. Resolves ONLY if all promises succeed. If even one fails, the whole thing instantly rejects (Short-circuits). Great for when you need all data to proceed.
  2. Promise.allSettled([p1, p2]): Runs all in parallel and waits for all of them to finish, regardless of whether they succeeded or failed. Returns an array showing the status of each.
  3. Promise.race([p1, p2]): Returns the result of the fastest promise, whether it succeeded or failed. Often used to create timeout functions for APIs.
  4. Promise.any([p1, p2]): Returns the result of the fastest promise that succeeds. It only rejects if every single promise fails.

While we use Promises all the time with modern APIs like fetch(), there are many times you will need to build your own Promise from scratch. This is usually done to wrap older, callback-based code (like setTimeout) into a modern, clean Promise architecture.

You create a Promise using the new Promise() constructor. It takes exactly one argument: a function called the executor. The executor function is handed two powerful built-in functions by JavaScript: resolve and reject.

const myFirstPromise = new Promise((resolve, reject) => {
    // 1. Do some asynchronous work here (e.g., a timer, a database call)
    let success = true;

    if (success) {
        // 2. If it works, call resolve() and pass it the data
        resolve("Operation was a success!");
    } else {
        // 3. If it fails, call reject() and pass it the error
        reject("Operation failed miserably.");
    }
});

A Promise’s state is immutable once settled. If you call resolve() and then immediately call reject(), JavaScript simply ignores the reject(). It locks in whichever happened first.

// Question: Write a sleep function using promises

// 1. We create a function that returns a constructed Promise
function sleep(milliseconds) {
    return new Promise((resolve, reject) => {
        
        // 2. We put the old-school async code inside the executor
        setTimeout(() => {
            // 3. We call resolve() when the timer finishes
            // We don't need to pass any data, just signaling it's done.
            resolve(); 
        }, milliseconds);
        
    });
}

// Now we can use it the modern way!
async function runProgram() {
    console.log("Loading...");
    await sleep(2000); // Pauses for 2 seconds
    console.log("Done!");
}

runProgram();

When you construct a promise, you have to decide what happens when it succeeds or fails. You do this using three methods that hook directly into the states we talked about above:

  • .then(data): Runs ONLY if the Promise hits the Fulfilled state.
  • .catch(error): Runs ONLY if the Promise hits the Rejected state (or if your code inside .then() throws a syntax error).
  • .finally(): Runs Always, whether it fulfilled or rejected. Great for cleaning up, like hiding a loading spinner.

What is the output of this code?

console.log("A");

const myPromise = new Promise((resolve, reject) => {
    console.log("B"); // Wait, is this async?
    resolve("C");
});

myPromise.then((data) => {
    console.log(data);
});

console.log("D");

Output: A, B, D, C We know .then() is asynchronous (it goes to the Microtask Queue). But the executor function inside new Promise() runs SYNCHRONOUSLY.

11. Advanced Functions & Execution Context

Whenever JavaScript runs code, it does so inside an Execution Context. Think of it as a temporary sandbox or container created to evaluate and execute a specific piece of code.

There are two main types:

  1. Global Execution Context (GEC): Created the moment your script starts. It creates the global window object and global this.
  2. Function Execution Context (FEC): A brand new sandbox created every single time a function is called.

Every time a context is created, it goes through a strict two-step process:

Phase 1: The Creation Phase (Memory Setup)

Before a single line of code is actually executed, the engine scans the code and sets up the Lexical Environment (the memory space).

  • It sets up the this keyword.
  • It looks for variable and function declarations (Hoisting happens here).
  • var variables are allocated memory and set to undefined.
  • let and const variables are allocated memory but are uninitialized (The Temporal Dead Zone).
  • Whole function declarations are stored in memory perfectly intact.

Phase 2: The Execution Phase (Running the Code)

Now the engine goes back to the top and reads the code line-by-line. It assigns the actual values to variables, and if it sees a function call, it pauses, creates a new Function Execution Context, and pushes it onto the Call Stack.

What is the Lexical Environment? It is the local memory of the current context, PLUS a hidden reference to the Lexical Environment of its parent. This reference forms the Scope Chain. If JS can’t find a variable in the local memory, it climbs the scope chain to look in the parent’s memory.

Closures (The Backpack)

A closure is the most famous consequence of how Execution Contexts work.

The Definition: A closure is a function that remembers its outer variables and can access them, even after the outer function has completely finished running and its Execution Context has been destroyed.

When a function is returned from another function, it doesn’t just return the code. It brings along a hidden “backpack” (the Lexical Environment) containing all the variables it needs from its parent.

function createCounter() {
    let count = 0; // This variable is in the outer function

    // We return an inner function
    return function increment() {
        count++; // It remembers 'count' from its parent!
        console.log(count);
    };
}

const myCounter = createCounter();

// createCounter has finished running. Its sandbox is completely gone.
// But increment() STILL has access to 'count' because of the Closure!

myCounter(); // 1
myCounter(); // 2
myCounter(); // 3

Why do we use Closures?

  • Data Privacy: In the example above, there is absolutely no way to access or modify count from the outside except by calling myCounter(). It acts as a perfectly private variable.
  • State Management: React hooks like useState rely heavily on closures to remember state between component renders.

Currying

Currying is a functional programming technique where you transform a function that takes multiple arguments f(a, b, c) into a sequence of nested functions that take one argument at a time f(a)(b)(c).

It relies entirely on Closures to work. The inner functions remember the arguments passed to the outer functions.

Standard Function:

function sum(a, b, c) {
    return a + b + c;
}
console.log(sum(1, 2, 3)); // 6

Curried Function:

function curriedSum(a) {
    return function(b) {
        return function(c) {
            return a + b + c; // Remembers 'a' and 'b' via closure
        };
    };
}
console.log(curriedSum(1)(2)(3)); // 6

Currying with Arrow Functions (The modern way):

const modernCurrySum = a => b => c => a + b + c;
console.log(modernCurrySum(1)(2)(3)); // 6

Why use Currying? It allows you to “pre-configure” functions.

const multiply = a => b => a * b;

// Pre-configure the first argument
const double = multiply(2);
const triple = multiply(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

IIFE (Immediately Invoked Function Expressions)

An IIFE (pronounced “iffy”) is a function that runs the exact moment it is defined. To create one, you wrap a function in parentheses () to tell JavaScript, “Treat this as an expression, not a standard declaration”, and then add () at the end to invoke it immediately.

(function() {
    const secret = "I am hidden!";
    console.log("This runs immediately!");
})();

// console.log(secret); // Error: secret is not defined

Before ES6 introduced let and const (which are block-scoped), var variables would leak out into the global scope. Developers used IIFEs to create isolated Execution Contexts so their variables wouldn’t collide with other scripts.

Today, they are mostly used when you want to use await at the top level of a file in older environments that don’t support Top-Level Await:

(async () => {
    const data = await fetch(url);
    console.log(data);
})();
// 1. Defined now, pushed to Call Stack LATER (when manually called)
function normalFunction() {
    console.log("I run later.");
}

// 2. Defined now, pushed to Call Stack NOW (runs immediately)
(function myIife() {
    console.log("I run immediately!");
})();

Modern Data Structures & Advanced Patterns (ES6+)

1. Map and WeakMap

An Object is technically a key-value store, but it has a massive limitation: Object keys can only be Strings or Symbols. If you try to use a number, an array, or another object as a key, JavaScript will silently convert it into the string "[object Object]".

Map

A Map is a pure dictionary. It allows any data type to be a key—including functions, objects, and arrays.

Why use a Map instead of an Object?

  1. Any Key Type: You can map DOM elements or objects to specific data without modifying the element itself.
  2. Maintains Order: A Map guarantees that the keys iterate in the exact order they were inserted. (Objects do not guarantee this).
  3. Built-in Size: You can easily check map.size. (With an object, you have to do Object.keys(obj).length).
  4. Performance: Maps are highly optimized for frequent additions and removals.
// ==========================
// Creating a Map
// ==========================

const map = new Map();

console.log(map);

// Output:
// Map(0) {}


// ==========================
// set()
// ==========================

const user = { name: "Alice" };

map.set(user, "Admin");
map.set("company", "OpenAI");
map.set(100, "Hundred");
map.set(true, "Yes");

console.log(map);

// Output:
// Map(4) {
//   { name: 'Alice' } => 'Admin',
//   'company' => 'OpenAI',
//   100 => 'Hundred',
//   true => 'Yes'
// }


// ==========================
// get()
// ==========================

console.log(map.get(user));
console.log(map.get("company"));

// Output:
// Admin
// OpenAI


// ==========================
// has()
// ==========================

console.log(map.has(user));
console.log(map.has("city"));

// Output:
// true
// false


// ==========================
// size
// ==========================

console.log(map.size);

// Output:
// 4


// ==========================
// keys()
// ==========================

for (const key of map.keys()) {
    console.log(key);
}

// Output:
// { name: 'Alice' }
// company
// 100
// true


// ==========================
// values()
// ==========================

for (const value of map.values()) {
    console.log(value);
}

// Output:
// Admin
// OpenAI
// Hundred
// Yes


// ==========================
// entries()
// ==========================

for (const [key, value] of map.entries()) {
    console.log(key, value);
}

// Output:
// { name: 'Alice' } Admin
// company OpenAI
// 100 Hundred
// true Yes


// ==========================
// Direct Iteration
// ==========================

for (const [key, value] of map) {
    console.log(key, value);
}

// Output:
// { name: 'Alice' } Admin
// company OpenAI
// 100 Hundred
// true Yes


// ==========================
// forEach()
// ==========================

map.forEach((value, key) => {
    console.log(key, value);
});

// Output:
// { name: 'Alice' } Admin
// company OpenAI
// 100 Hundred
// true Yes


// ==========================
// delete()
// ==========================

map.delete(100);

console.log(map);

// Output:
// Map(3) {
//   { name: 'Alice' } => 'Admin',
//   'company' => 'OpenAI',
//   true => 'Yes'
// }


// ==========================
// clear()
// ==========================

map.clear();

console.log(map);

// Output:
// Map(0) {}

WeakMap

A WeakMap is a specialized Map with two strict rules:

  1. Keys MUST be objects (no primitive values like strings or numbers).
  2. References are “weak”.

If the object acting as a key is deleted from your app (e.g., a DOM element is removed from the page), the WeakMap automatically throws away the value associated with it. This prevents massive memory leaks in long-running applications like React.

(Note: Because the engine could garbage-collect items at any time, WeakMaps are not iterable. You cannot use for...of or .size on them). Some methods of Map don’t exist on WeakMap.

2. Set and WeakSet

An Array allows duplicate values. A Set is a collection of strictly unique values.

Set

If you try to add a value to a Set that already exists, it simply ignores it. Sets use the === strict equality operator to check for duplicates under the hood.

Why use a Set?

  1. Removing Duplicates: The single most common use case for a Set is filtering duplicates out of an array.
  2. Fast Lookups: Checking if an item exists using set.has(value) is significantly faster than using array.includes(value).
// ==========================
// Creating a Set
// ==========================

const set = new Set();

console.log(set);

// Output:
// Set(0) {}


// ==========================
// add()
// ==========================

set.add(10);
set.add(20);
set.add(30);
set.add(20); // Duplicate
set.add("Hello");
set.add(true);

console.log(set);

// Output:
// Set(5) { 10, 20, 30, 'Hello', true }


// ==========================
// has()
// ==========================

console.log(set.has(20));
console.log(set.has(100));

// Output:
// true
// false


// ==========================
// size
// ==========================

console.log(set.size);

// Output:
// 5


// ==========================
// delete()
// ==========================

set.delete(20);

console.log(set);

// Output:
// Set(4) { 10, 30, 'Hello', true }


// ==========================
// values()
// ==========================

for (const value of set.values()) {
    console.log(value);
}

// Output:
// 10
// 30
// Hello
// true


// ==========================
// keys()
// (Same as values() for Set)
// ==========================

for (const key of set.keys()) {
    console.log(key);
}

// Output:
// 10
// 30
// Hello
// true


// ==========================
// entries()
// ==========================

for (const entry of set.entries()) {
    console.log(entry);
}

// Output:
// [10, 10]
// [30, 30]
// ['Hello', 'Hello']
// [true, true]


// ==========================
// Direct Iteration
// ==========================

for (const value of set) {
    console.log(value);
}

// Output:
// 10
// 30
// Hello
// true


// ==========================
// forEach()
// ==========================

set.forEach(value => {
    console.log(value);
});

// Output:
// 10
// 30
// Hello
// true


// ==========================
// clear()
// ==========================

set.clear();

console.log(set);

// Output:
// Set(0) {}

WeakSet

Similar to a WeakMap, a WeakSet can only store objects, and holds them weakly. It is mostly used to “tag” objects (e.g., keeping track of which DOM elements have already been processed by a script) without preventing those objects from being garbage collected.

3. Iterators and Generators

Normally, when you call a function, it runs from top to bottom and returns a single value. Generators are special functions that can pause their execution midway, spit out a value, and then resume right where they left off when you ask them to.

The Syntax

  • You define a generator by putting a star after the function keyword: function*
  • You use the yield keyword to pause the function and spit out a value.
// 1. Define the Generator
function* numberGenerator() {
    console.log("Starting...");
    yield 1; // Pauses here and returns 1
    
    console.log("Resuming...");
    yield 2; // Pauses here and returns 2
    
    console.log("Finished!");
    return 3;
}

// 2. Initialize it (This doesn't run the code yet!)
const gen = numberGenerator();

// 3. Step through it using .next()
console.log(gen.next()); 
// Logs: "Starting..."
// Returns: { value: 1, done: false }

console.log(gen.next()); 
// Logs: "Resuming..."
// Returns: { value: 2, done: false }

console.log(gen.next()); 
// Logs: "Finished!"
// Returns: { value: 3, done: true }

By default, you cannot use a for...of loop on a plain JavaScript object. If you try, JavaScript throws a TypeError: obj is not iterable.

However, JavaScript looks for a special hidden property called Symbol.iterator to know how to loop over something. By attaching a Generator to this symbol, you can completely customize how your object behaves in a loop.

const company = {
    name: "TechCorp",
    departments: {
        engineering: ["Alice", "Bob"],
        design: ["Charlie"],
        marketing: ["Dave", "Eve"]
    },
    // We attach a generator to define exactly what happens during a for...of loop
    [Symbol.iterator]: function* () {
        for (const dept in this.departments) {
            for (const employee of this.departments[dept]) {
                // We yield ONLY the employee names
                yield employee; 
            }
        }
    }
};

// Now we can loop over the object directly! 
// It magically knows to extract the employees out of the nested arrays.
for (const employee of company) {
    console.log(employee); 
}
// Output: "Alice", "Bob", "Charlie", "Dave", "Eve"

Debounce & Throttle

Debouncing (The “Elevator” Pattern)

The Concept: Imagine an elevator doors closing. If a person walks in, the doors reopen and the timer resets. The elevator only moves when X seconds have passed with nobody else entering. Use Case: A search bar auto-complete. You don’t want to make an API call for every single keystroke (“a”, “ap”, “app”, “appl”, “apple”). You want to wait until the user stops typing for 300ms, and then make one API call for “apple”.

function debounce(func, delay) {
    let timerId; // The closure remembers this variable

    return function (...args) {
        // If the user types again, we cancel the previous timer
        clearTimeout(timerId); 

        // Start a brand new timer
        timerId = setTimeout(() => {
            func.apply(this, args); 
        }, delay);
    };
}

// Usage:
const searchAPI = debounce((query) => console.log(`Fetching: ${query}`), 500);
// If a user types 10 letters rapidly, the API only fires once they pause for 500ms.

Throttling (The “Machine Gun” Pattern)

The Concept: A machine gun can only fire 5 bullets per second, no matter how fast you pull the trigger. Throttling guarantees a function will run at a steady, limited rate, ignoring any extra calls that happen in between. Use Case: A window resize or scroll listener. You want to update the UI while the user is scrolling, but only once every 200ms, rather than 100 times a second.

function throttle(func, limit) {
    let isThrottled = false; // The closure remembers this flag

    return function (...args) {
        if (!isThrottled) {
            // 1. Run the function immediately
            func.apply(this, args);
            isThrottled = true;

            // 2. Ignore all other calls until the limit expires
            setTimeout(() => {
                isThrottled = false; 
            }, limit);
        }
    };
}

// Usage:
window.addEventListener("scroll", throttle(() => console.log("Updating UI"), 200));

12. Modules: CommonJS vs. ESM

For a long time, JavaScript was the only major programming language that did not have a built-in way to share code between files. Today, modules are the absolute backbone of React, Angular, Vue, and modern Node.js.

Before modules, if you had multiple JavaScript files in an HTML document, they all shared the same Global Scope. If fileA.js had a variable named let count = 0;, and fileB.js also declared let count = 10;, your app would crash. A Module is simply a JavaScript file that strictly isolates its own variables and functions. Nothing leaks out, and nothing gets in unless you explicitly export it from one file and import it into another.

Because JavaScript took so long to create an official module system, the community built its own. This resulted in two competing standards that you will see in codebases today.

System A: CommonJS (The Node.js Pioneer)

Created in 2009 specifically for Node.js. It was designed for servers, meaning it reads files synchronously directly from the hard drive.

  • Exporting: You attach things to a global module.exports object.
  • Importing: You use the require() function.
// math.js (CommonJS Export)
function add(a, b) {
    return a + b;
}
const PI = 3.14;

// Bundle them into an object and export it
module.exports = { add, PI }; 
// app.js (CommonJS Import)
const math = require('./math.js'); // Synchronous pause until loaded
console.log(math.add(2, 3));

System B: ECMAScript Modules (ESM – The Modern Standard)

Introduced in 2015 (ES6), this is the official JavaScript standard. It was designed for the web, meaning it loads files asynchronously (so it doesn’t freeze the browser while waiting for a file to download).

  • Exporting: You use the export keyword.
  • Importing: You use the import keyword.

If you are writing modern React or Node, you will be using ESM. There are two distinct ways to export data from a file, and understanding the difference is crucial.

1. Named Exports (For multiple items)

You can have as many named exports in a file as you want. When importing them, the names must match exactly, and they must be wrapped in curly braces {}.

// utils.js (Exporting)
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;

// app.js (Importing)
import { add, subtract } from './utils.js';

The Rename Trick (as): If the name conflicts with a variable you already have, you can rename it during the import.

import { add as mathAdd } from './utils.js';

The Import All Trick (*): If a file has 50 named exports and you want them all, you can pack them into a single object.

import * as MathUtils from './utils.js';
console.log(MathUtils.add(5, 5));

2. Default Exports (For a single main item)

A file can only have one default export. Because there is only one, you don’t need curly braces to import it, and you can name it whatever you want when you import it.

// User.js (Exporting)
export default class User {
    constructor(name) { this.name = name; }
}

// app.js (Importing - notice no curly braces!)
import User from './User.js'; 

// You can even name it something completely different
import AwesomeUserClass from './User.js'; 

(Note: You can safely mix one default export and several named exports in the same file! React uses this constantly: import React, { useState, useEffect } from 'react';)

4. Dynamic Imports (Lazy Loading)

Standard import statements must go at the absolute top of your file. But what if you only want to load a massive PDF-rendering module if the user clicks the “Download PDF” button? You can use import() as a function. It returns a Promise, allowing you to load code asynchronously exactly when you need it.

button.addEventListener('click', async () => {
    // The browser only downloads pdfMaker.js when this button is clicked!
    const pdfModule = await import('./pdfMaker.js');
    pdfModule.generatePDF();
});

Because the two systems (CommonJS and ESM) are completely incompatible, you have to explicitly tell your environment that you are using modern ESM.

In the Browser:

You must add type="module" to your script tag. This also automatically applies Strict Mode ("use strict") and defers the script loading so it doesn’t block HTML parsing.

<script type="module" src="app.js"></script>

In Node.js:

By default, Node assumes all files are legacy CommonJS. To use ESM, you have two options:

  1. Change your file extensions from .js to .mjs (Module JavaScript).
  2. Add "type": "module" to your project’s package.json file (This is the industry standard).

6. Why ESM Won (Tree Shaking)

Aside from being the official language standard, ESM won the module war because it is statically analyzable.

Because import statements are strictly at the top of the file, build tools (like Webpack or Vite) can look at your code before it runs and see exactly which functions you used and which ones you ignored.

If you import a massive 100,000-line library, but only import one add() function, the build tool will completely delete the other 99,990 lines from the final code you send to the user. This is called Tree Shaking, and it is impossible with the older CommonJS system.

13. Typed Arrays

Standard JavaScript arrays are incredibly flexible—you can put a string, a number, and an object all in the same array, and it will dynamically resize itself as you add more items.

But that flexibility comes at a huge performance cost. When dealing with raw binary data like WebGL for 3D graphics, processing audio files, reading binary streams from WebSockets, or manipulating image pixels in an HTML <canvas> standard arrays are much too slow and bloated.

To handle raw bytes at lightning speed, JavaScript introduced Typed Arrays.

A standard array is just one thing: const arr = [].

A Typed Array separates the concept of memory into two distinct parts:

  1. The ArrayBuffer: A raw chunk of memory (the bytes).
  2. The View (Typed Array): A lens that tells JavaScript how to read and write to those bytes.

Step 1: ArrayBuffer (The Memory Block)

An ArrayBuffer is just a fixed-length contiguous block of raw memory.

// Ask the system for exactly 16 bytes of memory.
// It initializes filled with zeros.
const buffer = new ArrayBuffer(16);

console.log(buffer.byteLength); // 16

You cannot read or write data directly to an ArrayBuffer. If you try to do buffer[0] = 5, it will just fail silently or do nothing useful. It is purely raw RAM. To touch the data, you need a View.

Step 2: The View (How you read it)

A view overlays the buffer and formats it into numbers. Depending on the view you choose, you tell JavaScript how many bytes make up a single number.

  • Int8Array: Treats every 1 byte as a number (from -128 to 127).
  • Uint8Array: Treats every 1 byte as an unsigned number (0 to 255). This is the most common one for raw binary data and image pixels.
  • Int16Array: Treats every 2 bytes as a number.
  • Int32Array: Treats every 4 bytes as a number.
  • Float64Array: Treats every 8 bytes as a high-precision decimal number.
const buffer = new ArrayBuffer(16); // 16 total bytes

// Create a view that groups bytes into blocks of 4 (32-bit integers)
const view32 = new Int32Array(buffer); 

// Because 16 divided by 4 is 4, this array holds exactly 4 numbers.
console.log(view32.length); // 4

// Now we can assign values like a normal array
view32[0] = 42;
view32[1] = 999;

console.log(view32); // Int32Array(4) [42, 999, 0, 0]

The true superpower of an ArrayBuffer is that you can attach multiple different views to the exact same block of memory at the same time. If you update the memory using one view, the other view instantly reflects the change because they are looking at the same physical bytes.

Let’s say we have a 4-byte buffer:

const buffer = new ArrayBuffer(4);

// View 1: Read the 4 bytes as a single 32-bit number
const view32 = new Uint32Array(buffer);

// View 2: Read the exact same 4 bytes as four 8-bit numbers
const view8 = new Uint8Array(buffer);

// Let's set the single 32-bit number to something large
view32[0] = 4294967295; // (This is the max value for 32-bit: all 1s in binary)

// Now look at the 8-bit view. It automatically updated!
console.log(view8); 
// Uint8Array(4) [255, 255, 255, 255]

Because Typed Arrays represent fixed hardware memory, they have strict limitations compared to normal [] arrays:

  1. No Dynamic Resizing: You cannot use .push(), .pop(), .shift(), or .unshift(). If you need a bigger array, you have to allocate a brand new, larger ArrayBuffer and copy the old data over.
  2. No Missing Values: There is no such thing as an “empty” slot. If you don’t assign a value, it defaults to 0. (You will never see undefined in a Typed Array).
  3. Strict Typing: If you try to put a string or an object into a Typed Array, it will force-convert it to a number (usually 0), or throw an error.
  4. Overflow Behavior: If you try to put the number 300 into a Uint8Array (which maxes out at 255), it won’t crash. It “wraps around” and becomes 44 (because 300 - 256 = 44).

Sometimes, dealing with pure Typed Arrays isn’t enough especially if you are receiving data over a network from a server that uses a different computer architecture (specifically, Endianness).

A DataView is a special, highly flexible view that lets you read and write multiple data types at specific byte offsets within the same buffer, while giving you strict control over Endianness.

const buffer = new ArrayBuffer(16);
const view = new DataView(buffer);

// Write an 8-bit integer at byte offset 0
view.setInt8(0, 42);

// Write a 16-bit integer starting at byte offset 1
view.setInt16(1, 999);

// Read the 16-bit integer back
console.log(view.getInt16(1)); // 999

You will rarely need DataView unless you are doing low-level network programming or parsing custom binary file formats like MP4 headers.

We will talk about other concepts in a different tutorial such as DOM, and WebAPIs.

Support

Buy author a coffee

Support
SummarizeSendShareTweetScan
Previous Post

Git Tutorial: A Beginners Guide To Git And GitHub

Neuraldemy

Neuraldemy

This is Neuraldemy support. Subscribe to our YouTube channel for more.

Related Posts

Git Tutorial: A Beginners Guide To Git And GitHub

Why RAM Prices Will Continue To Rise?

100 CSS Interview Questions

100 CSS Interview Questions

FormData API: A Beginner’s Guide to Native Validation

Strategy Pattern In JavaScript

Factory Pattern In JavaScript

Support

Buy author a coffee

Support

Support Neuraldemy

Newsletter

Top rated products

  • Probability and Statistics for Machine Learning and Data Science Probability and Statistics for Machine Learning and Data Science
    Rated 5.00 out of 5
    30.00$ Original price was: 30.00$.12.99$Current price is: 12.99$.
  • SVM Notes: Optimization & Implementation SVM Notes: Optimization & Implementation
    Rated 5.00 out of 5
    9.99$ Original price was: 9.99$.4.99$Current price is: 4.99$.
  • spidy mail hunter software Spidy Mail Hunter: Powerful And Intelligent Website & PDF Email Extractor Software (Windows App)
    Rated 4.78 out of 5
    149.00$ Original price was: 149.00$.49.00$Current price is: 49.00$.
  • Linear Algebra For Machine Learning And Data Science Linear Algebra For Machine Learning And Data Science
    Rated 4.71 out of 5
    40.00$ Original price was: 40.00$.24.99$Current price is: 24.99$.
  • Clustering and Outlier Detection A Comprehensive Tutorial Clustering and Outlier Detection 20.97$ Original price was: 20.97$.13.47$Current price is: 13.47$.

Recent Posts

Javascript Tutorial

JavaScript Concepts Revision For Interview

Git Tutorial: A Beginners Guide To Git And GitHub

Why RAM Prices Will Continue To Rise?

July 2026
M T W T F S S
 12345
6789101112
13141516171819
20212223242526
2728293031  
« Mar    

Neuraldemy

Neuraldemy

Neuraldemy

Software Development

Neuraldemy helps you learn ML, AI, Web Dev and data science from scratch. Addtionally, we also provide web development and hosting services for businesses, and individuals

  • Get Started
  • Contact
  • Book Services
  • Privacy Policy
  • Terms Of Use
  • Support Portal
  • Managed Hosting Terms
  • Web Development Terms
  • Refund Policy
Neuraldemy

© 2026 - A learning platform by Odist Magazine

Welcome Back!

Login to your account below

Forgotten Password? Sign Up

Create New Account!

Fill the forms below to register

*By registering into our website, you agree to the Terms & Conditions and Privacy Policy.
All fields are required. Log In

Retrieve your password

Please enter your username or email address to reset your password.

Log In
No Result
View All Result
Join Our WhatsApp Channel
  • Home
  • Tutorials
    • Machine Learning
    • Python
    • Web Development
    • Javascript
    • Mathematics
    • Deep Learning
    • Artificial Intelligence
  • Store
  • Our Services
    • Managed Hosting Services
    • Web Development Services
  • Our Softwares
    • JaggoRe AI
    • Email Extractor
    • TimeWell App
  • Login
  • Sign Up
  • Cart
Order Details

© 2026 - A learning platform by Odist Magazine

This website uses cookies. By continuing to use this website you are giving consent to cookies being used.
0