Unit 3 - Notes
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
- Global Execution Context (GEC): The default context where code resides that is not inside any function. It creates the global object (
windowin browsers,globalin Node.js) and sets thethiskeyword equal to that global object. - Function Execution Context (FEC): Created whenever a function is invoked (not defined). Each function call creates a new context.
- Eval Execution Context: Code executed inside an
evalfunction (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.
- Script starts: GEC is pushed to the bottom of the stack.
- Function called: New FEC is pushed on top.
- 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:
functiondeclarations are stored fully in memory.varvariables are initialized asundefined.letandconstare 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.
thisbinding: The value ofthisis 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
ReferenceErroroccurs.
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
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
- Data Privacy / Emulating Private Methods: Hiding implementation details and exposing only a public API (Module Pattern).
- Function Factories: Creating functions with preset configurations (Currying).
- Memoization: Caching results of expensive function calls.
- 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:
- JS checks if the object has the property (Own Property).
- If not, it looks at the object's
__proto__. - It continues up the chain until it finds the property or reaches
null.
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:
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
- Call Stack: Executes synchronous code.
- Web APIs (Browser) / C++ APIs (Node): Handles tasks like
setTimeout, DOM events, and HTTP requests (fetch). These run outside the main JS thread. - Callback Queue (Task Queue): Holds callbacks from async operations waiting to be executed.
- 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
console.log('Start')-> Stack -> Execute -> Pop.setTimeout(...)-> Stack -> Offloaded to Web API -> Stack Pop.console.log('End')-> Stack -> Execute -> Pop.- Timer finishes in Web API -> Callback moves to Queue.
- 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
- Macrotasks (Task Queue):
setTimeout,setInterval,setImmediate(Node), I/O operations, UI rendering. - Microtasks: Promises (
.then,.catch,.finally),queueMicrotask,MutationObserver,process.nextTick(Node - technically higher priority than promises).
Event Loop Execution Order
- Execute all synchronous code in the script (Call Stack).
- 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.
- Render: The browser may update the UI/render.
- Process ONE Macrotask: The Event Loop picks the oldest task from the Macrotask queue and pushes it to the stack.
- Repeat.
Example
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:
- Pending: Initial state.
- Fulfilled (Resolved): Operation completed successfully.
- Rejected: Operation failed.
Consuming Promises:
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.
asyncfunction: Always returns a Promise.await: Pauses the execution of the async function until the Promise is resolved.- Error Handling: Uses standard
try...catchblocks.
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)
varis function-scoped (or global).letandconstare block-scoped (scoped to the nearest{}).constprevents 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 ownthis. They inheritthisfrom the surrounding scope. Useful in callbacks and event listeners.
Destructuring
Extracting values from arrays or properties from objects into distinct variables.
// 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: Returnsundefinedinstead of throwing an error ifaddressis null/undefined.foo ?? 'default': Returns 'default' only iffooisnullorundefined(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).
// 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.
// 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.