Unit 3 - Notes

INT219 9 min read

Unit 3: Advanced JavaScript and Asynchronous Programming

1. Execution Context and Scope Chain

JavaScript code does not run directly; it runs inside an environment called the Execution Context. Understanding this is fundamental to understanding hoisting, scope, and closure.

Types of Execution Contexts

  1. Global Execution Context (GEC): The default context where code resides that is not inside any function. It creates the global object (window in browsers, global in Node.js) and sets the this keyword equal to that global object.
  2. Function Execution Context (FEC): Created whenever a function is invoked (not defined). Each function call creates a new context.
  3. Eval Execution Context: Code executed inside an eval function (rarely used due to security/performance concerns).

The Execution Stack (Call Stack)

JavaScript is single-threaded. It uses a LIFO (Last In, First Out) stack to manage execution contexts.

  1. Script starts: GEC is pushed to the bottom of the stack.
  2. Function called: New FEC is pushed on top.
  3. Function returns: FEC is popped off the stack.

Phases of Execution Context

Every execution context goes through two phases:

A. Creation Phase (Memory Creation)

Before code execution, the engine scans the code:

  • Variable Object (VO): Memory is allocated for variables and functions.
  • Hoisting:
    • function declarations are stored fully in memory.
    • var variables are initialized as undefined.
    • let and const are placed in the Temporal Dead Zone (TDZ) and are not initialized (accessing them causes a ReferenceError).
  • Scope Chain Creation: Links to outer lexical environments are established.
  • this binding: The value of this is determined.

B. Execution Phase

The engine runs through the code line by line, assigning values to variables and executing function calls.

Scope and Scope Chain

  • Lexical Scope: Scope is determined by the physical location of the code definition. Inner functions have access to outer function variables.
  • Scope Chain: If a variable is not found in the current scope (local memory), the engine looks up the chain to the parent's lexical environment, continuing until the Global Scope. If not found there, a ReferenceError occurs.

2. Closures

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).

Core Concept

In JavaScript, functions form a closure around the data they are defined in. This allows an inner function to access variables from an outer function even after the outer function has finished executing and popped off the call stack.

Code Example

JAVASCRIPT
function outerFunction(outerVariable) {
    const secret = "I am hidden";

    return function innerFunction(innerVariable) {
        console.log(`Outer: ${outerVariable}`);
        console.log(`Inner: ${innerVariable}`);
        console.log(`Secret: ${secret}`); // Accessing variable from closed-over scope
    };
}

const newFunction = outerFunction("outside");
// outerFunction has finished execution here.
// Yet, newFunction retains access to 'outerVariable' and 'secret'.

newFunction("inside"); 
// Output: 
// Outer: outside
// Inner: inside
// Secret: I am hidden

Practical Use Cases

  1. Data Privacy / Emulating Private Methods: Hiding implementation details and exposing only a public API (Module Pattern).
  2. Function Factories: Creating functions with preset configurations (Currying).
  3. Memoization: Caching results of expensive function calls.
  4. Iterators: Maintaining state across asynchronous calls.

3. Prototype and Prototype Chain

JavaScript is a prototype-based language, meaning objects inherit properties and methods directly from other objects.

__proto__ vs prototype

  • prototype: A property that exists on constructor functions (and classes). It is the template used to build the __proto__ of instances created by that constructor.
  • __proto__ (or [[Prototype]]): A property that exists on instances (objects). It points to the prototype of the constructor that created it.

The Prototype Chain

When accessing a property on an object:

  1. JS checks if the object has the property (Own Property).
  2. If not, it looks at the object's __proto__.
  3. It continues up the chain until it finds the property or reaches null.

JAVASCRIPT
function Animal(name) {
    this.name = name;
}

Animal.prototype.speak = function() {
    console.log(`${this.name} makes a noise.`);
};

const dog = new Animal("Rex");

// Look up process for dog.speak():
// 1. Does 'dog' have 'speak'? No.
// 2. Does dog.__proto__ (Animal.prototype) have 'speak'? Yes. Execute it.
dog.speak(); // Rex makes a noise.

Prototypal Inheritance

Before ES6 Classes, inheritance was achieved manually:

JAVASCRIPT
function Dog(name) {
    Animal.call(this, name); // Super constructor call
}

// Link prototypes
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function() {
    console.log("Woof!");
};


4. Event Loop and Concurrency Model

JavaScript uses a non-blocking, event-driven concurrency model. While the engine is single-threaded (one call stack), the browser (or Node.js) provides additional threads via Web APIs to handle asynchronous operations.

Key Components

  1. Call Stack: Executes synchronous code.
  2. Web APIs (Browser) / C++ APIs (Node): Handles tasks like setTimeout, DOM events, and HTTP requests (fetch). These run outside the main JS thread.
  3. Callback Queue (Task Queue): Holds callbacks from async operations waiting to be executed.
  4. Event Loop: A mechanism that constantly monitors the Call Stack and the Callback Queue.
    • Rule: If the Call Stack is empty, the Event Loop takes the first item from the Queue and pushes it onto the Stack.

Visual Flow

  1. console.log('Start') -> Stack -> Execute -> Pop.
  2. setTimeout(...) -> Stack -> Offloaded to Web API -> Stack Pop.
  3. console.log('End') -> Stack -> Execute -> Pop.
  4. Timer finishes in Web API -> Callback moves to Queue.
  5. Event Loop sees Stack is empty -> Moves Callback from Queue to Stack -> Execute.

5. Microtask and Macrotask Queues

Not all asynchronous tasks are treated equally. The Event Loop prioritizes the Microtask Queue over the Macrotask (Task) Queue.

The Hierarchy

  1. Macrotasks (Task Queue): setTimeout, setInterval, setImmediate (Node), I/O operations, UI rendering.
  2. Microtasks: Promises (.then, .catch, .finally), queueMicrotask, MutationObserver, process.nextTick (Node - technically higher priority than promises).

Event Loop Execution Order

  1. Execute all synchronous code in the script (Call Stack).
  2. Process ALL Microtasks: The Event Loop checks the Microtask queue. It runs all pending microtasks until the queue is empty. If a microtask adds another microtask, that acts recursively and is run immediately.
  3. Render: The browser may update the UI/render.
  4. Process ONE Macrotask: The Event Loop picks the oldest task from the Macrotask queue and pushes it to the stack.
  5. Repeat.

Example

JAVASCRIPT
console.log('1'); // Sync

setTimeout(() => console.log('2'), 0); // Macrotask

Promise.resolve().then(() => console.log('3')); // Microtask

console.log('4'); // Sync

// Output: 1, 4, 3, 2
// Explanation: 
// 1, 4 run synchronously.
// Stack empties.
// Event Loop checks Microtasks: runs 3.
// Microtasks empty.
// Event Loop checks Macrotasks: runs 2.


6. Promises and Asynchronous Control Flow

Promises

A Promise is an object representing the eventual completion or failure of an asynchronous operation. It solves the "Callback Hell" problem.

States:

  1. Pending: Initial state.
  2. Fulfilled (Resolved): Operation completed successfully.
  3. Rejected: Operation failed.

Consuming Promises:

JAVASCRIPT
const myPromise = new Promise((resolve, reject) => {
    // async logic
    if (success) resolve("Data");
    else reject("Error");
});

myPromise
    .then(data => console.log(data)) // Handles success
    .catch(err => console.error(err)) // Handles error
    .finally(() => console.log("Done")); // Runs regardless of outcome

Static Methods

  • Promise.all([p1, p2]): Waits for all to resolve. If one rejects, the whole call rejects immediately.
  • Promise.allSettled([p1, p2]): Waits for all to finish, regardless of status. Returns an array of objects with status/value.
  • Promise.race([p1, p2]): Returns the result of the first promise to settle (resolve OR reject).
  • Promise.any([p1, p2]): Returns the first fulfilled promise. Ignores rejections unless all reject.

Async / Await

Introduced in ES8 (ES2017), this is syntactic sugar built on top of Promises. It makes async code look synchronous.

  • async function: Always returns a Promise.
  • await: Pauses the execution of the async function until the Promise is resolved.
  • Error Handling: Uses standard try...catch blocks.

JAVASCRIPT
async function fetchData() {
    try {
        const response = await fetch('/api/data');
        const data = await response.json();
        console.log(data);
    } catch (error) {
        console.error("Fetch failed", error);
    }
}


7. ES6+ Language Features

Block Scoping (let and const)

  • var is function-scoped (or global).
  • let and const are block-scoped (scoped to the nearest {}).
  • const prevents reassignment (though objects declared with const can still be mutated).

Arrow Functions

  • Concise syntax: const add = (a, b) => a + b; (implicit return).
  • Lexical this: Arrow functions do not have their own this. They inherit this from the surrounding scope. Useful in callbacks and event listeners.

Destructuring

Extracting values from arrays or properties from objects into distinct variables.

JAVASCRIPT
// Object
const user = { name: "Alice", age: 25 };
const { name, age } = user;

// Array
const coords = [10, 20];
const [x, y] = coords;

Spread (...) and Rest (...) Operators

  • Spread: Expands an iterable (like an array) into individual elements. Used for cloning/merging arrays and objects.
    • const newArr = [...oldArr, 4, 5];
  • Rest: Condenses multiple elements into a single array element. Used in function parameters.
    • function sum(...args) { return args.reduce((a,b) => a+b); }

Template Literals

String interpolation using backticks and ${} syntax. Supports multiline strings.

Optional Chaining (?.) & Nullish Coalescing (??)

  • user?.address?.street: Returns undefined instead of throwing an error if address is null/undefined.
  • foo ?? 'default': Returns 'default' only if foo is null or undefined (unlike || which triggers on falsy values like 0 or empty strings).

8. JavaScript Modules

Modules allow code to be split into separate files, improving maintainability and namespace management.

Evolution

  • IIFE (Immediately Invoked Function Expressions): The old way to simulate private scope.
  • CommonJS: Used in Node.js (require / module.exports). Synchronous loading.
  • ES Modules (ESM): The official standard for JavaScript (Browser and Node).

ES Modules Syntax

1. Named Exports:
Can export multiple values. Must be imported with the specific name (in curly braces).

JAVASCRIPT
// math.js
export const add = (a, b) => a + b;
export const sub = (a, b) => a - b;

// main.js
import { add, sub } from './math.js';

2. Default Exports:
One default export per file. Can be named anything when importing.

JAVASCRIPT
// User.js
export default class User { ... }

// main.js
import UserClass from './User.js'; // Name chosen by importer

Features of ES Modules

  • Strict Mode: Modules execute in strict mode ("use strict") by default.
  • Defer: Module scripts in browsers (<script type="module">) are deferred automatically (they wait for HTML parsing).
  • Tree Shaking: Modern bundlers (Webpack, Rollup) can remove unused exports from the final bundle to reduce file size.
  • Singleton: Modules are evaluated only once. Subsequent imports share the same instance.