JavaScript Fundamentals

Kishore MongarKishore Mongar
28 min read

JavaScript is a synchronous single-threaded language.

It’s considered a loosely typed or dynamically typed language because it doesn't require you to explicitly define the type of a variable when you declare it. The type of a variable is determined at runtime based on the value assigned to it. This means that variables in JavaScript can change types during the course of the program. This provides flexibility but also requires developers to be more cautious about potential type-related issues.

1. Execution context and Call stack

Everything in JavaScript happens in execution context(EC), i.e, the environment in which JavaScript code is evaluated and executed. The context keeps track of variables, functions, and the scope chain for that function. EC has two main component :-

  1. Memory (Variable environment) - Where all the variables and functions are stored as key-value pairs.

  2. Code (Thread of execution) - Where code is executed one line at a time

Execution Context (EC)

Memory(Variable environment)Code(Thread of execution)
key: value--------- code--------------
a : 10---------code---------
fn: {....}-------code---------------

EC is created in two phases:-

Phase 1 - Memory creation(allocation) - JavaScript will allocate all the variables and functions.

  • Create variable bindings (for var, let, and const variables).

  • Set them to undefined for variables declared with var (or leave them in a "temporal dead zone" for let and const).

  • Create function declarations and allocate memory for them, meaning their names are added to the scope.

Phase 2 - Code execution - After memory has been allocated, the code is executed line by line.

  • The values are assigned to variables (for var), and functions are executed when encountered.

  • For functions, the function body is executed when called.

The key distinction between phase 1 and phase 2 is that phase 1 prepares the environment for execution by creating memory spaces, while phase 2 is where the actual execution happens.

Note - Whenever the function is invoked the execution context is created. The ‘return’ keyword tells the function to give the control back to the execution context, where the function was invoked. When the whole function is executed the entirety of execution context is also deleted.

How this code is executed ?

var n = 2;

function square(num){
  var ans = num * num;
  return ans;
};

var square2 = square(n);
var square4 = square(4);

Phase 1 - Memory creation(allocation)

Global Execution Context (GEC)

Memory(Variable environment)Code(Thread of execution)
n : undefinedvar n
square: {whole code of fn is stored..}function square(num){
var ans = num * num;
return ans;
};
square2 : undefinedvar square2
square4 : undefinedvar square4

Phase 2 - Code execution

Global Execution Context (GEC)

Memory(Variable environment)Code(Thread of execution)
n : 2var n = 2;
square: {whole code of fn is stored..}function square(num){
var ans = num * num;
return ans;
};
square2 : 4EC is created and after return the assignment of value is set.
square4 : 16Similarly EC is created and after return the assignment of value is set.

square2

EC Phase 1

Memory(Variable environment)Code(Thread of execution)
num: undefinednum(parameter)
ans: undefinedvar ans = num * num;

EC Phase 2

Memory(Variable environment)Code(Thread of execution)
num: 2n = 2
ans: 4num * num;

Call stack - Maintains the order in which execution contexts are processed. It handles the creation and removal of these contexts, and controls their flow using the stack.

Call stack synonyms - Execution context stack, Program stack, Control stack, Runtime stack, Machine stack.

Call stack
EC3
EC2
EC1
GEC

2. Hoisting

Hoisting in JavaScript is the default behavior where variable and function declarations are moved(hoisted) to the top of their scope during the compilation phase (memory allocation).

console.log(myVar);  // undefined
var myVar = 5;

sayHello();  // "Hello!"
function sayHello() {
  console.log("Hello!");
}
TypeHoistedInitializationBehaviorScope
varDeclaration is hoistedInitialized to undefined initiallyCan be accessed before declaration (but value is undefined)Function Scope or Global Scope
let / constDeclaration is hoistedNot initialized until assignedThrows ReferenceError if accessed before assignment (temporal dead zone)Block Scop
Function DeclarationEntire function (name + body) is hoistedFunction body is ready to be executedCan be called before the declarationFunction Scope or Global Scope
Function Expression (var, let, const)Only the variable declaration is hoistedFunction is not available until assignedCannot be called before assigned (ReferenceError)Function Scope or Block Scope

Notes:

  • Function scope means the variable or function is scoped within the function.

  • Block scope means the variable is scoped to the nearest block (like loops, conditionals, etc.).

Difference between not defined and undefined ?

  • not defined - The variable doesn’t exist in memory at all. Trying to access it results in a ReferenceError.

  • undefined - It’s a placeholder value and the variable exists(declared), but no value has been assigned(initialized) to it yet. It holds the value undefined.

3. Window and this

The Global Execution Context sets up the global environment, including the window object, and determines the behavior of this. When JavaScript code is executed in a browser, a Global Execution Context is created. Inside this context:

  • The window object is created and serves as the global object that holds all global variables and functions.

  • The this keyword points to the window object in the global context.

  • Any global variable or function is attached to the window object.

window - It’s a global object that represents the browser's window. It serves as a global container for variables, functions, and other objects in the global scope. When you run JavaScript in a browser, all global variables and functions are essentially attached to the window object. For example:

var myVar = "Hello";
console.log(window.myVar); // Outputs "Hello"

Additionally, window provides methods for interacting with the browser's environment, such as window.alert() or window.setTimeout().

this - It’s a special keyword, refers to the context in which the function is executed. Its value depends on how a function is called, and it can refer to different things based on the execution context.

  • In a global context (outside of any function), this refers to the global object (window in browsers).

      console.log(this); // In browsers, it will log the `window` object
    
  • Inside a function, this refers to the object that invoked the function. If the function is called as a method of an object, this refers to that object.

      const obj = {
        name: "Alice",
        greet: function() {
          console.log(this.name); // "this" refers to the obj object
        }
      };
      obj.greet(); // Logs "Alice"
    
  • In arrow functions, this is lexically bound, meaning it retains the value of this from the surrounding context (e.g., the global object in the case of a function defined in the global scope).

      const obj = {
        name: "Bob",
        greet: () => {
          console.log(this.name); // In arrow functions, "this" is not bound to obj
        }
      };
      obj.greet(); // Logs undefined (because "this" refers to the global object, not obj)
    

this Keyword Inside Functions (Strict Mode and Non-Strict Mode)

  • In non-strict mode: If this is used inside a regular function and it's not explicitly set (e.g., via call, apply, or bind), it will refer to the global object (window in browsers).

  • In strict mode: The value of this inside a function is undefined if it's not explicitly bound to anything. Strict mode helps avoid accidental bugs that occur when this refers to the global object.

      function nonStrict() {
        console.log(this);  // In non-strict mode, `this` refers to the global object (window).
      }
      nonStrict();  // In a browser, this logs the window object.
    
      'use strict';
      function strictMode() {
        console.log(this);  // In strict mode, `this` is undefined.
      }
      strictMode();  // Logs undefined
    

this Inside Object Method - When this is used inside a method of an object, it refers to the object itself. This allows you to access other properties and methods of that object.

const person = {
  name: 'John',
  greet: function() {
    console.log(`Hello, ${this.name}`);  // `this` refers to the person object.
  }
};

person.greet();  // Output: "Hello, John"

this Inside Arrow Functions - Unlike regular functions, arrow functions do not have their own this. Instead, they lexically inherit this from the enclosing scope (the context in which the arrow function is defined).

This means that arrow functions do not create their own this but instead inherit it from the outer function or global context.

const person = {
  name: 'Alice',
  greet: function() {
    const innerGreet = () => {
      console.log(`Hello, ${this.name}`);  // `this` is lexically bound to `person`.
    };
    innerGreet();
  }
};

person.greet();  // Output: "Hello, Alice"

Nested Arrow Function - Arrow functions inside other arrow functions will continue to use the same this from the outer function, as it’s lexically bound.

const obj = {
  name: 'Nested Arrow',
  method: function() {
    const nested = () => {
      const inner = () => {
        console.log(this.name);  // `this` is inherited from the enclosing `method`.
      };
      inner();
    };
    nested();
  }
};

obj.method();  // Output: "Nested Arrow"

this with call(), apply(), and bind()

call() and apply() allow you to invoke a function immediately and overwrite the this context.

  • call(object, a1,a2,a3): Pass arguments separately.

  • apply(object, [a1,a2,a3]): Pass arguments as an array.

bind(): Returns a new function with a permanently bound this, which can be invoked later.

These methods are powerful tools for sharing methods between objects and for controlling the value of this dynamically.

  1. call() and apply(): These methods allow you to explicitly set the value of this when calling a function. call() takes arguments separately, while apply() takes arguments as an array.

     //**call()**
     const person = {
       name: 'Bob'
     };
    
     function greet() {
       console.log(`Hello, ${this.name}`);
     }
    
     greet.call(person);  // Output: "Hello, Bob"
    
     //**apply()**
     const numbers = [1, 2, 3];
     function sum(a, b, c) {
       console.log(a + b + c);
     }
    
     sum.apply(null, numbers);  // Output: 6
    
  2. bind(): This method returns a new function where this is explicitly set to the value passed to bind(). It doesn’t execute the function immediately but creates a bound function that can be called later.

     const person = {
       name: 'Charlie'
     };
    
     function greet() {
       console.log(`Hello, ${this.name}`);
     }
    
     const boundGreet = greet.bind(person);
     boundGreet();  // Output: "Hello, Charlie"
    

this in the DOM - When you use this inside event handlers in the DOM, it refers to the HTML element that triggered the event.

<button id="myButton">Click me</button>
<script>
  const button = document.getElementById('myButton');

  button.addEventListener('click', function() {
    console.log(this);  // `this` refers to the button element that was clicked.
  });
</script>
  • If the above code is executed and the button is clicked, this inside the event handler will refer to the button element itself.

“this“ substitution - It’s all about how JavaScript decides what this should point to based on the context in which the function is called.

this Substitution Rules:

  1. Global Function Call:

    • Non-strict mode: this refers to the global object (e.g., window in browsers).

    • Strict mode: this is undefined.

  2. Method Call: this refers to the object the method is called on.

  3. Constructor Call (new keyword): this refers to the newly created object.

  4. Arrow Functions: this is lexically inherited from the surrounding context (it doesn’t have its own this).

  5. call(), apply(), bind(): These methods allow you to explicitly set this.

  6. DOM Event Handlers: this refers to the DOM element that triggered the event.

4. Scope, Lexical environment and Scope chain

The lexical environment is crucial for understanding closures, and the scope chain is a way of resolving variable lookups by moving from the innermost scope to the outer scopes.

Helps in understanding how JavaScript handles variable resolution and closures, and how execution contexts play a role in function execution.

Scope - Where you can access a specific variable or a function in the code. Scope is also directly related to Lexical environment.

  • Scope refers to the context in which a variable is defined and accessible in JavaScript.

  • Variables and functions in JavaScript are available in specific regions of your code, and these regions are determined by scope.

There are two main types of scope:

  • Global Scope: The outermost scope, where variables and functions are accessible throughout the entire program.

  • Local (Function) Scope: The scope inside a function, where variables and parameters defined within the function are only accessible inside that function.

  • Block Scope: Variables declared with let and const are scoped to the nearest block (like inside loops or if statements).

let x = 10;  // global scope
function foo() {
  let y = 20;  // local scope
  console.log(x);  // x is accessible here
}
console.log(y);

function testBlockScope() {
  if (true) {
    let blockScopedVar = "I'm block scoped!";
    const anotherBlockVar = "I'm also block scoped!";
  }

  console.log(blockScopedVar); // Error: blockScopedVar is not defined
  console.log(anotherBlockVar); // Error: anotherBlockVar is not defined
}
testBlockScope();

Lexical(Nest/Hierarchy) Environment

  • Lexical Environment is the environment where a piece of code (such as a function) is defined. It’s like a snapshot of the scope at the time of the function's creation.

  • In simpler terms, it refers to the location where a variable or function is declared, and this determines the scope chain when the function is executed.

Example:

function outer() {
  let outerVar = "I'm outside!";
  function inner() {
    console.log(outerVar); // inner() has access to outerVar due to lexical scoping
  }
  inner();
}
outer();

Here, inner() has access to the outerVar because it was defined in the lexical scope of outer().

Scope Chain - When you try to access a variable, JavaScript starts looking for that variable in the current function's local scope. If it doesn’t find it there, it moves to the outer function's scope, and so on, until it reaches the global scope. The Scope Chain refers to the hierarchical structure that JavaScript uses to resolve variable references.

The scope chain allows nested functions to access variables from their outer functions, and ultimately from the global environment.

Example:

let globalVar = "I am global";

function outer() {
  let outerVar = "I am outer";

  function inner() {
    let innerVar = "I am inner";
    console.log(innerVar);  // Accesses innerVar (local to inner)
    console.log(outerVar);  // Accesses outerVar (from outer scope)
    console.log(globalVar); // Accesses globalVar (from global scope)
  }
  inner();
}
outer();

In this example, inner() can access innerVar, outerVar, and globalVar due to the scope chain:

  • First, it looks for innerVar in the inner function.

  • If it doesn't find it there, it looks up to the outer function for outerVar.

  • Finally, it looks in the global scope for globalVar.

Note :- Whenever a execution context is created, a lexical environment(Local memory along with lexical environment of it’s parent) is also created.

Closures

A function bind together with its lexical environment,

  • Closures are a feature that arises from the combination of scope and lexical environment. A closure is a function that "remembers" the scope in which it was created, even after the outer function has finished execution.

  • Closures allow inner functions to maintain access to variables in their lexical environment, even after the outer function has returned.

Example:

function outer() {
  let count = 0;
  return function inner() {
    count++;
    console.log(count);
  };
}

const counter = outer();
counter(); // 1
counter(); // 2
counter(); // 3

In this example, the inner function maintains access to the count variable from outer() even after outer() has returned.

Use cases: Module design pattern, currying, functions like once, memoize, maintaining state in async world, setTimeouts, Iterators, and many more,

Example with var:

It creates a closure but var has function-scoping (not block-scoping), meaning the variable is shared across all iterations of the loop. So, if you use var inside a loop, the value of the variable is overwritten in each iteration, and by the time the setTimeout function executes, the value will be the final one from the loop.

for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // Prints 3 three times
  }, 1000);
}

//Resolution with var and closure to have it's own scope
for (var i = 0; i < 3; i++) {
  (function (i) {
    setTimeout(function() {
      console.log(i); // Prints 0, 1, 2
    }, 1000);
  })(i);
}
  • Explanation: In the example above, by the time the setTimeout callback executes, the value of i will be 3 (the loop finishes at i = 3). This happens because var is function-scoped, not block-scoped. All setTimeout functions share the same reference to i, which ends up being 3 after the loop finishes.

On the other hand, let has block scoping. Each time the loop iterates, it creates a new scope, and each setTimeout callback captures a new value of i. This means the setTimeout callback will log the correct value for each iteration.

Example with let:


for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i); // Prints 0, 1, 2
  }, 1000);
}

Explanation: Here, let is block-scoped, so each iteration of the loop creates a new binding for i. Each setTimeout callback captures the correct value of i at the time the loop runs. Therefore, the console will log 0, 1, and 2, each after 1 second.

Closures in JavaScript have some potential disadvantages:

  1. Memory Leaks: Closures can prevent garbage collection by keeping references to variables that are no longer needed.

  2. Performance Issues: Excessive use of closures, especially in loops, can lead to higher memory usage and slower performance.

  3. Debugging Challenges: Tracking variable scope can be difficult, making it harder to debug complex code.

  4. Unintended Side Effects: Closures may unintentionally retain old data, leading to unexpected behavior.

  5. Increased Complexity: Overusing closures can make code harder to understand and maintain.

5. Let & Const

The variables let and const are hoisted but are in temporal dead zone for the time being, placed in a separate "execution context"

Both let and const are hoisted to the top of their block or function scope but remain uninitialized until their actual declaration is encountered during code execution. During this "dead zone," any access to the variable results in a ReferenceError.

Temporal Dead Zone refers to the period when these variables are hoisted but are still in an uninitialized state.

ReferenceError: Occurs when JavaScript tries to reference a variable or function that doesn't exist or isn't defined in the current scope.

SyntaxError: Happens when there’s an issue with the code’s syntax or grammar. Common causes are misspelled keywords, missing parentheses, braces, or brackets or incorrect use of operators or other syntax issues.

TypeError: Triggered when an operation is performed on a value of an inappropriate type or when a value is not of the expected type, like calling a method on null or undefined. Performing an operation between incompatible types.

6. Block, block scope and shadowing

Block - A block is a section of code enclosed within curly braces {}. It groups a set of statements together. Blocks are used in structures like conditionals (if, else), loops (for, while), and functions. Also known as compound statement.

{
    let x = 5;
    console.log(x);  // x is accessible inside this block
}

Block scope - It refers to the visibility and lifetime of variables declared with let or const. A variable declared within a block (i.e., within the {}) is only accessible inside that block and its nested blocks. Once the block ends, the variable goes out of scope, meaning it cannot be accessed outside the block.

  • Variables declared with let and const are confined to the block in which they are declared, making them block-scoped.
if (true) {
    let x = 10;   // x is block-scoped
    const y = 20;  // y is block-scoped
    console.log(x); // 10
    console.log(y); // 20
}
// Outside the block, both x and y are not accessible
console.log(x);  // Error: x is not defined
console.log(y);  // Error: y is not defined

Shadowing - Occurs when a variable declared in an inner scope (like a block or function) has the same name as a variable declared in an outer scope. In this case, the inner variable "shadows" or "overrides" the outer variable within the inner scope.

let x = 10;   // Outer variable

if (true) {
    let x = 20;  // Inner variable with the same name, shadowing outer x
    console.log(x);  // 20 (inner variable is accessed here)
}

console.log(x);  // 10 (outer variable is accessed here)

var gets shadowed

var a = 10;  // global variable

if (true) {
    var a = 20;  // shadowing the global `a` inside the block
    console.log(a);  // 20 (accesses the inner a)
}

console.log(a);  // 20 (global `a` is overwritten by inner `a`)

let and const does not get shadowed in outer scope.

let a = 10;
if (true) {
    let a = 20;  // shadowing the outer a inside the block
    console.log(a);  // 20 (accesses the inner a)
}

console.log(a);  // 10 (outer a remains unaffected)

Note: JavaScript engines creates execution contexts (ECs) is the key to understanding the behavior of let, const, and var.

  • let and const are block-scoped, and each block creates its own execution context, keeping variables confined to that block and preventing them from affecting the outer scope.

  • var, however, is function-scoped (or global-scoped), and its variable is hoisted to the global execution context (or function execution context), making it accessible outside the block. This is why var can overwrite or shadow outer variables, especially when declared inside a block.

Illegal shadowing - However, re-declaring a variable with let or const in the same scope (including the same block) is illegal because it results in an error as these keywords respect block-level scoping rules.. This means you can't have two variables with the same name declared with let or const within the same scope.

Example of illegal shadowing with let and const:

let x = 10;
let x = 20;  // Error: Cannot redeclare block-scoped variable 'x'

//const
const y = 30;
const y = 40;  // Error: Cannot redeclare block-scoped variable 'y

Example with const:

var z = 50;
var z = 60;  // No error, but overwrites the previous value of z
console.log(z);  // 60

7. Functions

In JavaScript, functions can be defined in several ways, and hoisting plays an important role in how they behave during execution.

I) Function statement (function declaration) - A function declaration defines a named function. The function is hoisted in its entirety, meaning the function's definition (including the body) is moved to the top of the scope during the compilation phase.

Hoisting Behavior: The function myFunc is hoisted completely, so it can be called before its declaration in the code.

console.log(myFunc()); // Outputs: "Hello, world!"

function myFunc() {
  return "Hello, world!";
}

II) Function expression - A function expression is when you assign a function to a variable. Only the variable declaration is hoisted, not the function definition. This means you cannot call the function before it is defined.

Hoisting Behavior: The variable add is hoisted, but its assignment to the function happens at runtime, so calling add before its definition results in an error.

const add = function(a, b) {
  return a + b;
};

console.log(add(3, 4));  // Output: 7

III) Arrow Function Expression - Arrow functions are similar to function expressions but use a more concise syntax. Like regular function expressions, only the variable declaration is hoisted, not the function assignment. Hoisting Behavior: The behavior is the same as a regular function expression. The variable myFunc is hoisted, but the function itself is not accessible before its assignment.

console.log(myFunc()); // Error: Cannot call `myFunc` before initialization

const myFunc = () => {
  return "Hello, world!";
};

IV) Anonymous Function - An anonymous function is a function that does not have a name. It is usually assigned to a variable or passed as an argument to another function. It's often used in function expressions or as callbacks. Hoisting Behavior: The variable myFunc is hoisted (but its value is not), so you cannot call the function before it is defined in the code. In other words, the function itself is not hoisted, just the variable declaration.

console.log(myFunc()); // Error: myFunc is not a function

const myFunc = function() {
  return "Hello from anonymous function!";
};

// Anonymous Function used as a callback
setTimeout(function() {
  console.log('This is an anonymous function!');
}, 1000);

V) Named Function Expression - A named function expression is a function expression that has a name. The name can be used for debugging purposes (e.g., in stack traces) or to refer to the function within itself (for recursion). Hoisting Behavior: Similar to an anonymous function, the variable (myFunc) is hoisted but the function itself is not hoisted. However, the name of the function (myFunction in this case) is accessible only within the body of the function itself, not in the outer scope.

const myFunc = function myFunction() {
  console.log("This is a named function expression!");
}

console.log(myFunc()); // Outputs: "This is a named function expression!"

In JavaScript, first-class functions (or first-class citizens) refers to the concept that functions in JavaScript can be treated like any other variable. This gives JavaScript its functional programming capabilities and allows for powerful patterns like closures, higher-order functions, and callbacks. This means that functions can be:

  1. Assigned to variables.

  2. Passed as arguments to other functions.

  3. Returned from other functions.

  4. Stored in data structures (e.g., arrays, objects).

// Function assigned to a variable
const square = function(x) {
  return x * x;
};

// Function passed as an argument
function operateOnNumber(num, operation) {
  return operation(num);
}

console.log(operateOnNumber(5, square));  // Output: 25

// Function returned from another function
function outer() {
  return function inner() {
    return "Inner function";
  };
}

const innerFunction = outer();
console.log(innerFunction());  // Output: "Inner function"

8. Event listeners & callback functions

Callback Functions: A callback function is a function passed into another function as an argument to be executed later. This mechanism enables asynchronous behavior, allowing non-blocking operations like timers and event handling.

  setTimeout(function () {
    console.log("Timer");
  }, 1000); // The function is executed after 1000 milliseconds

Event Listeners: Event listeners in JavaScript are used to handle user interactions like clicks, key presses, etc. They rely on callback functions (forms a closure because it retains access to variables from the outer scope) to define the actions to be taken when an event occurs.

//Normal event listeners
document.getElementById("myButton").addEventListener("click", function() {
    alert("Button clicked!");
});

//counter fn forms a closure retaining the value of count from the outer scope   
function clickBtn() {
  let count = 0;
  document.getElementById("myButton").addEventListener("click", function counter() {
    console.log("Button clicked!", ++count);
  });
}

clickBtn();

Here, the anonymous function serves as a callback that runs when the button with the ID "myButton" is clicked.

By leveraging callback functions, JavaScript allows for efficient handling of asynchronous tasks and event-driven programming, enhancing the responsiveness and interactivity of applications.

9. Web APIs

Web APIs are built-in browser features that JavaScript can interact with. These APIs are part of the browser's runtime environment and provide essential functionality for building web applications.

  1. setTimeout - Used to run code after a delay (timers).

  2. DOM APIs - Used to interact with and manipulate the HTML structure (e.g., getElementById, addEventListener).

  3. fetch() - Used for making HTTP requests to retrieve resources asynchronously.

  4. localStorage() - Provides a way to store data locally in the browser persistently.

  5. console - Used for logging information to the browser console (e.g., log(), error(), warn()).

  6. location - Provides details and control over the current URL and page navigation (e.g., href, reload()).

Execution Flow The Event Loop, Callback Queue, and Call Stack are key concepts in JavaScript's concurrency model. Understanding how these work together is crucial for grasping how JavaScript handles asynchronous operations and event-driven programming.

  • Call Stack: Keeps track of currently executing functions.

  • Callback Queue(Task Queue): Holds callbacks for asynchronous tasks like setTimeout, events, or API calls(fetch() or XMLHttpRequest once their response is ready.).

  • Microtask Queue: Holds tasks like promise .then(), .catch(), or tasks added using queueMicrotask(). These tasks have higher priority than those in the callback queue. Promises, MutationObserver and queueMicrotask()

  • Event Loop: Monitors the call stack and callback queue. When the call stack is empty, the event loop moves functions from the callback queue to the call stack for execution. Ensures that the event-driven (asynchronous) code runs after the synchronous code completes.

console.log("Start");

setTimeout(() => {
  console.log("Callback from setTimeout");
}, 0);

Promise.resolve().then(() => {
  console.log("Microtask 1");
}).then(() => {
  console.log("Microtask 2");
});

console.log("End");

//output
//Start
//End
//Microtask 1
//Microtask 2
//Callback from setTimeout

Execution Flow with Microtasks:

Let’s break down how JavaScript handles a mix of synchronous code, microtasks, and regular tasks (callbacks from setTimeout() or event listeners):

console.log("Start");

setTimeout(() => {
  console.log("Callback from setTimeout");
}, 0);

Promise.resolve().then(() => {
  console.log("Microtask 1");
}).then(() => {
  console.log("Microtask 2");
});

console.log("End");

10. JS Runtime Environment

The JavaScript runtime environment is where JavaScript code is executed. It consists of two main environments:

  1. Browser Environment: The runtime in web browsers (e.g., Chrome, Firefox, Safari).

  2. Node.js Environment: A runtime built on Chrome’s V8 JavaScript engine that allows JavaScript to be executed outside the browser (e.g., for server-side scripting).

The JavaScript engine is responsible for interpreting and executing JavaScript code. Both browsers and Node.js use a JavaScript engine, but the engines might differ.

  • Browser Engines: Examples include V8 (Chrome), SpiderMonkey (Firefox), Chakra (Edge), and JavaScriptCore (Safari).

  • Node.js uses the V8 engine, which is the same as Google Chrome's engine.

The Execution Process of JavaScript Code

When JavaScript code is executed, it follows several steps to ensure the code is properly parsed, compiled, and then executed.

Code → Parsing → Compilation → Execution

  • Code: The JavaScript code is written.

  • Parsing: The code is parsed into tokens and an Abstract Syntax Tree (AST) is generated.

  • Compilation (JIT): The parsed code is compiled into bytecode and optimized just before execution.

  • Execution: The optimized machine code is executed by the engine.

JIT compilation is a key feature of modern JavaScript engines. It involves:

  • Bytecode generation: Initially, the JavaScript code is compiled into an intermediate form (bytecode).

  • Optimization: As the program runs, the engine continuously optimizes the bytecode for better performance.

  • Machine code: The optimized bytecode is finally translated into machine code, which the CPU can execute directly.

11. Higher-Order Functions

A function that either takes one or more functions as arguments or returns a function as its result.

For example, map, filter, and reduce

// Example: map is a higher-order function
const numbers = [1, 2, 3, 4];
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8]

Functional Programming - Functional programming emphasizes immutability, first-class functions, and pure functions. In FP, functions are treated as first-class citizens, meaning they can be passed as arguments, returned from other functions, and assigned to variables.

Why Use Higher-Order Functions?

  • Abstraction: They allow you to abstract common logic, making code more modular and reusable.

  • Composability: Functions can be composed together to create more complex behavior without writing verbose code.

Benefits of Using Higher-Order Functions in JavaScript:

  • Makes code more readable and concise.

  • Enhances reusability by separating concerns.

  • Encourages the use of pure functions that avoid side effects.

Common Use-Cases:

  • Event Handlers: Higher-order functions are used extensively in handling events, like addEventListener in the DOM.

  • Callback Functions: Many JavaScript APIs and libraries make use of higher-order functions to handle asynchronous tasks.

12. Callback

Importance of Callbacks: Callbacks facilitate non-blocking, asynchronous behavior in JavaScript, allowing tasks like I/O operations to run concurrently without freezing the user interface.

Challenges with Callbacks:

  • Callback Hell: Nesting multiple callbacks can lead to complex, hard-to-read code structures, often referred to as the "Pyramid of Doom."

  • Inversion of Control: Passing callbacks to external functions can result in unpredictable execution sequences, making the codebase difficult to manage.

13. Promises

Promises address several challenges associated with callbacks, such as callback hell and inversion of control.

Promises: Promises are objects representing the eventual completion (or failure) of an asynchronous operation. They provide a cleaner alternative to callbacks by allowing chaining and structured error handling.

Promise States: A Promise can be in one of three states:

  • Pending: The initial state; the operation is ongoing.

  • Fulfilled: The operation completed successfully.

  • Rejected: The operation failed.

Creating Promises: Promises are created using the Promise constructor, which takes an executor function with resolve and reject callbacks to handle success and failure, respectively.

Promise Chaining: Promises allow chaining of .then() methods to handle sequences of asynchronous operations, improving code readability and structure rather than callback hell. In each .then(), we return another Promise. This allows the next .then() in the chain to wait for the result of the previous one.

Error Handling: Errors in Promises can be caught using the .catch() method, providing a centralized place to handle exceptions.

// Function that returns a Promise
function checkWeather(isRaining) {
  return new Promise((resolve, reject) => {
    if (isRaining) {
      reject("It's raining, take an umbrella!");
    } else {
      resolve("The weather is clear, you can go outside!");
    }
  });
}

// Using the Promise
checkWeather(false)
  .then((message) => {
    console.log(message); // Output: The weather is clear, you can go outside!
  })
  .catch((error) => {
    console.log(error); // This would run if there was an error (e.g., if isRaining was true)
  });

Different Types of Promise APIs:

  1. Promise.all Takes an array of promises and returns an array of their resolved values when all of them are resolved. If any promise is rejected, it immediately returns that error. If any promise in the array rejects, the .catch will be triggered immediately.
const promise1 = Promise.resolve(3);
const promise2 = Promise.resolve(5);
const promise3 = new Promise((resolve, reject) => setTimeout(resolve, 100, 7));

Promise.all([promise1, promise2, promise3])
  .then(values => console.log(values))  // [3, 5, 7]
  .catch(error => console.error(error));
  1. Promise.allSettledWaits for all promises to settle, regardless of whether they resolve or reject. It returns an array of objects indicating the status of each promise.
const promise1 = Promise.resolve(3);
const promise2 = Promise.reject('Error!');
const promise3 = new Promise((resolve) => setTimeout(resolve, 100, 7));

Promise.allSettled([promise1, promise2, promise3])
  .then(results => console.log(results));

//Output
// [
//  { status: 'fulfilled', value: 3 },
//  { status: 'rejected', reason: 'Error!' },
//  { status: 'fulfilled', value: 7 }
// ]
  1. Promise.raceTakes an array of promises and returns the first one to settle (either resolved or rejected).
  • Promise2 resolves first, so it's the result returned by Promise.race.
javascript
Copy
const promise1 = new Promise((resolve) => setTimeout(resolve, 100, 'First!'));
const promise2 = new Promise((resolve) => setTimeout(resolve, 50, 'Second!'));

Promise.race([promise1, promise2])
  .then(result => console.log(result));  // Output: "Second!"
  1. Promise.anyTakes an array of promises and resolves when the first promise is successfully resolved, ignoring any rejections. If all promises are rejected, it throws an AggregateError with all the rejection reasons.
  • Even though promise1 and promise2 are rejected, promise3 resolves first, so it returns "Success!".
const promise1 = Promise.reject('Error 1');
const promise2 = Promise.reject('Error 2');
const promise3 = new Promise((resolve) => setTimeout(resolve, 100, 'Success!'));

Promise.any([promise1, promise2, promise3])
  .then(result => console.log(result))  // Output: "Success!"
  .catch(err => console.error(err));

Note - Asynchronous Javascript - handles using Callback, Promises and Async/Await.

Async/Await - A syntactic sugar over promises that makes asynchronous code look and behave more like synchronous code. async is used to define a function that returns a promise, and await is used to pause the function execution until a promise resolves.

async function myAsyncFunction() {
    console.log("Start of the async function");
    await new Promise(resolve => setTimeout(resolve, 1000)); // Simulate delay
    console.log("End of the async function");
}

myAsyncFunction();

//or

async function getData() {
    const response = await fetch('<https://jsonplaceholder.typicode.com/todos/1>');
    const data = await response.json(); // Convert the response to JSON
    console.log(data); // Logs the data
}

getData();
0
Subscribe to my newsletter

Read articles from Kishore Mongar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Kishore Mongar
Kishore Mongar

I am passionate about creating seamless web experiences with modern technologies. My unwavering commitment to staying at the forefront of the industry is fueled by a goal for continuous learning and professional development.