JavaScript moves declarations to top of their scope during compilation
Only declarations hoisted, not initializations
Temporal Dead Zone (TDZ)
Time between entering scope and variable initialization where access causes error
Exists for let, const, and class
TDZ Start
Beginning of enclosing block scope
Not visible in code; conceptual time period
TDZ End
Point where variable is declared and initialized
Variable becomes usable after this point
Example: Hoisting and TDZ behavior
// var hoisting: declaration hoisted, initialized to undefinedconsole.log(x); // undefined (no error)var x = 5;console.log(x); // 5// let/const TDZ: declaration hoisted but not initialized// console.log(y); // ReferenceError: Cannot access 'y' before initializationlet y = 10;console.log(y); // 10// Function declaration: fully hoistedgreet(); // "Hello" (works before declaration)function greet() { console.log("Hello");}// Function expression: only variable hoisted// sayHi(); // TypeError: sayHi is not a functionvar sayHi = function() { console.log("Hi");};sayHi(); // "Hi"// TDZ example with block scope{ // TDZ starts here for 'temp' // console.log(temp); // ReferenceError // const result = doSomething(temp); // ReferenceError let temp = 5; // TDZ ends here console.log(temp); // 5 (now accessible)}// Class TDZ// const p = new Person(); // ReferenceErrorclass Person { constructor(name) { this.name = name; }}const p = new Person("John"); // Works after declaration
Note: TDZ prevents using variables before initialization, catching potential bugs. Always
declare variables at the top of their scope for clarity.
2. Scope Types (Global, Function, Block, Module)
Scope Type
Created By
Variables
Lifetime
Access
Global Scope
Outside all functions/blocks
var, let, const, functions
Entire program execution
Accessible everywhere
Function Scope
Function declarations/expressions
var, parameters, inner functions
Function execution
Within function and nested functions
Block Scope
{ } braces, loops, if/switch
let, const, class
Block execution
Within block only
Module Scope
ES6 modules (.js files with import/export)
All declarations in module file
Module lifetime
Isolated; explicit exports required
Catch Block Scope
catch(error) { }
Error parameter, let, const
Catch block execution
Within catch block only
Declaration
Global Scope
Function Scope
Block Scope
var
✓ Function or global
✓ Function-scoped
❌ Ignores blocks
let
✓ Global (no window property)
✓ Function-scoped
✓ Block-scoped
const
✓ Global (no window property)
✓ Function-scoped
✓ Block-scoped
function
✓ Global
✓ Function-scoped
❌ Block-scoped in strict mode only
Example: Different scope types
// Global scopevar globalVar = 'global var';let globalLet = 'global let';const globalConst = 'global const';function demoScopes() { // Function scope var functionVar = 'function scoped'; let functionLet = 'function scoped'; console.log(globalVar); // Accessible if (true) { // Block scope var blockVar = 'not block scoped (var)'; let blockLet = 'block scoped'; const blockConst = 'block scoped'; console.log(functionVar); // Accessible from outer function console.log(blockLet); // Accessible within block } console.log(blockVar); // Accessible (var ignores blocks) // console.log(blockLet); // ReferenceError (block-scoped) // Loop scope for (let i = 0; i < 3; i++) { // 'i' is block-scoped to this loop setTimeout(() => console.log(i), 100); // Prints 0, 1, 2 } for (var j = 0; j < 3; j++) { // 'j' is function-scoped (shared) setTimeout(() => console.log(j), 100); // Prints 3, 3, 3 }}// Module scope (in ES6 modules)// Variables not automatically global// export const moduleVar = 'exported';// const privateVar = 'not exported';// Catch block scopetry { throw new Error('test');} catch (error) { // 'error' is scoped to catch block let catchVar = 'catch scoped'; console.log(error.message);}// console.log(error); // ReferenceError// console.log(catchVar); // ReferenceError
Warning: Global variables create properties on the global object (window in
browsers) with var, but not with let/const. Avoid global variables to
prevent naming conflicts.
3. Lexical Scope and Scope Chain
Concept
Description
Behavior
Lexical Scope
Scope determined by code location (where functions are written)
Inner functions access outer function variables
Scope Chain
Hierarchy of nested scopes searched for variable resolution
Searches from inner → outer until found or reaches global
Static Scoping
Another name for lexical scoping (opposite of dynamic scoping)
Scope determined at write-time, not runtime
Outer Environment Reference
Link from execution context to parent scope
Created when function is defined, not when called
Variable Lookup Process
Step
Action
Step 1
Current Scope
Check if variable exists in current local scope
Step 2
Parent Scope
If not found, move to enclosing (parent) scope
Step 3
Repeat
Continue up the scope chain through ancestors
Step 4
Global Scope
Check global scope as last resort
Step 5
Not Found
ReferenceError if not found anywhere in chain
Example: Lexical scope and scope chain
// Scope chain demonstrationconst globalVar = 'global';function outer() { const outerVar = 'outer'; function middle() { const middleVar = 'middle'; function inner() { const innerVar = 'inner'; // Scope chain: inner → middle → outer → global console.log(innerVar); // Found in inner scope console.log(middleVar); // Found in middle scope (parent) console.log(outerVar); // Found in outer scope (grandparent) console.log(globalVar); // Found in global scope // Variable lookup order: // 1. Check inner scope // 2. Check middle scope // 3. Check outer scope // 4. Check global scope // 5. ReferenceError if not found } inner(); // console.log(innerVar); // ReferenceError (not in scope chain) } middle();}outer();// Lexical scope is determined by code structurefunction makeCounter() { let count = 0; // In outer function scope return function() { // Lexically scoped to access 'count' from parent return ++count; };}const counter1 = makeCounter();console.log(counter1()); // 1console.log(counter1()); // 2const counter2 = makeCounter();console.log(counter2()); // 1 (separate scope chain)// Scope determined at definition, not invocationlet x = 'global x';function showX() { console.log(x); // Lexically bound to global 'x'}function demo() { let x = 'local x'; showX(); // Prints "global x" (not "local x") // Scope determined where showX was defined}demo();
Note: Lexical scope is determined by where the function is written, not where it's called. This enables closures
and predictable variable access.
4. Variable Shadowing and Name Resolution
Concept
Description
Behavior
Recommendation
Variable Shadowing
Inner variable hides outer variable with same name
Inner scope variable takes precedence
Avoid shadowing; use different names
Name Resolution
Process of finding which variable identifier refers to
Searches scope chain from inner to outer
Use descriptive, unique names
Parameter Shadowing
Function parameter shadows outer variable
Parameter accessible, outer variable hidden
Common pattern; usually intentional
Block Shadowing
Block-scoped variable shadows outer scope
Only within block; outer restored after
Use for temporary local variables
Shadowing Type
Outer
Inner
Allowed?
Behavior
var shadows var
var
var
✓ Yes
Redeclaration in same scope or shadowing in nested
let/const shadows var
var
let/const
✓ Yes
Inner hides outer; outer unchanged
var shadows let/const
let/const
var
Depends
Error if same function scope; OK if nested function
let/const shadows let/const
let/const
let/const
✓ In different block
✗ SyntaxError in same block
Parameter shadowing
Any
Parameter
✓ Yes
Parameter has priority in function
Example: Variable shadowing scenarios
// Basic shadowinglet x = 'outer';function demo() { let x = 'inner'; // Shadows outer 'x' console.log(x); // "inner"}demo();console.log(x); // "outer" (outer unchanged)// Parameter shadowinglet value = 100;function processValue(value) { // Parameter shadows outer 'value' console.log(value); // Uses parameter, not outer variable value = 200; // Modifies parameter only}processValue(50); // Prints 50console.log(value); // 100 (outer unchanged)// Block shadowinglet count = 10;if (true) { let count = 20; // Shadows outer 'count' in block console.log(count); // 20}console.log(count); // 10 (outer restored after block)// Nested function shadowingfunction outer() { let name = 'outer'; function inner() { let name = 'inner'; // Shadows parent function's 'name' console.log(name); // "inner" } inner(); console.log(name); // "outer"}outer();// Loop variable shadowingfor (let i = 0; i < 3; i++) { console.log(i); // Loop 'i' for (let i = 0; i < 2; i++) { console.log(' ' + i); // Inner loop 'i' shadows outer }}// Problematic shadowing to avoidfunction calculateTotal() { let total = 0; if (true) { let total = 100; // Shadowing can be confusing // Which 'total' is which? } return total; // Returns 0, not 100}
Warning: While shadowing is allowed, excessive use can make code confusing. Use distinct
variable names for clarity unless shadowing is intentional (e.g., function parameters).
5. Closure Patterns and Memory Management
Concept
Definition
Key Characteristics
Closure
Function bundled with its lexical environment (surrounding state)
Retains access to outer scope variables even after outer function returns
Closure Creation
Created every time a function is created
Inner function "closes over" variables from outer scope
Closure Use Cases
Data privacy, factory functions, callbacks, event handlers
Maintains state between function calls
Memory Retention
Closed-over variables remain in memory
Can cause memory leaks if not managed properly
Closure Pattern
Use Case
Example Structure
Private Variables
Encapsulation, data hiding
Function returns methods that access private variables
Note: Closures are powerful but retain references to their outer scope. Be mindful of memory
usage, especially with long-lived closures or large captured variables.
6. Module Scope and Variable Isolation
Module Feature
Description
Benefit
Module Scope
Each ES6 module has its own scope (file-level)
Variables don't pollute global scope by default
Variable Isolation
Module variables are private unless explicitly exported
Prevents naming conflicts between modules
Strict Mode
Modules automatically run in strict mode
No need for 'use strict'; directive
Top-level this
this is undefined at module top level
Not bound to global object (safer)
Static Imports
Import declarations hoisted and executed first
Enables static analysis and tree-shaking
Module Pattern
Syntax
Use Case
Named Export
export const x = 1;
Export multiple items from module
Default Export
export default function() {}
Main export from module (one per module)
Named Import
import { x, y } from './mod';
Import specific exports
Default Import
import mod from './mod';
Import default export
Namespace Import
import * as mod from './mod';
Import all exports as object
Re-export
export { x } from './mod';
Export from another module (barrel pattern)
Dynamic Import
import('./mod').then(...)
Load modules conditionally/lazily
Example: Module scope and isolation
// ===== math.js (Module A) =====// Private variables (not exported)const PI = 3.14159;let calculationCount = 0;// Private helper functionfunction log(operation) { calculationCount++; console.log(`Operation: ${operation}, Count: ${calculationCount}`);}// Named exports (public API)export function add(a, b) { log('add'); return a + b;}export function multiply(a, b) { log('multiply'); return a * b;}export const TAU = 2 * PI; // Export constant// Default exportexport default function calculate(expr) { return eval(expr); // Example only}// ===== user.js (Module B) =====// Module variables are isolatedconst userCount = 0; // Won't conflict with other modulesclass User { constructor(name) { this.name = name; }}export { User, userCount };// ===== app.js (Main module) =====// Import from math.jsimport calculate, { add, multiply, TAU } from './math.js';// Import from user.jsimport { User, userCount } from './user.js';// Module-scoped variables (private to this module)const appName = 'MyApp';let config = { debug: true };console.log(add(5, 3)); // 8console.log(multiply(2, 4)); // 8console.log(TAU); // 6.28318// Cannot access private variables from math.js// console.log(PI); // ReferenceError// console.log(calculationCount); // ReferenceErrorconst user = new User('Alice');// Top-level 'this' is undefined in modulesconsole.log(this); // undefined (not window)// ===== Dynamic imports =====async function loadModule() { if (config.debug) { const debugModule = await import('./debug.js'); debugModule.log('Debug mode enabled'); }}// ===== Barrel export pattern (index.js) =====// Re-export from multiple modulesexport { add, multiply } from './math.js';export { User } from './user.js';export { default as calculate } from './math.js';
Module Benefits:
Encapsulation: private implementation details
Explicit dependencies: clear import statements
No global pollution: isolated scope
Reusability: modular, composable code
Static analysis: enables tree-shaking
Before Modules (Legacy):
// IIFE pattern for isolation(function() { var private = 'hidden'; window.MyModule = { public: function() { return private; } };})();
Note: ES6 modules provide true encapsulation with file-level scope. Variables are private by
default, eliminating need for IIFE patterns and reducing global scope pollution.
Section 3 Summary
Hoisting moves declarations to top of scope;
let/const/class have TDZ (ReferenceError before initialization)
4 scope types: Global (everywhere), Function (var), Block (let/const),
Module (file-level isolation)
Lexical scope determined by code structure; scope chain searches inner →
outer → global for variables
Variable shadowing occurs when inner scope variable hides outer one; use
distinct names for clarity
Closures retain access to outer scope variables; enable private state but
require memory management
ES6 modules provide true isolation with file-level scope; variables private
unless exported