Online Compiler logoOnline Compiler

JavaScript Scope & Scope Chain: Complete Guide

What You'll Learn:

  • ✅ What is scope and why it matters in JavaScript
  • ✅ Types of scope: global, function, block, and lexical scope
  • ✅ How the scope chain works internally
  • ✅ Variable lookup process step-by-step
  • ✅ Common scope pitfalls and how to avoid them
  • ✅ Interview questions about scope

What is Scope?

Scope determines where variables are accessible in your code. Every variable has a scope – a specific region of code where that variable exists and can be used. Variables declared in one scope are not accessible outside that scope, which prevents naming conflicts and improves code organization.

Think of scope like a house with rooms. Variables declared in one room (scope) aren't automatically accessible in other rooms. However, you can look into the parent room (parent scope) from your current room, creating a chain of accessibility.

Types of Scope in JavaScript

1. Global Scope

Variables declared outside any function or block have global scope. They are accessible everywhere in your code – in functions, blocks, and nested functions. In browsers, global variables become properties of the `window` object. In Node.js, they become properties of the `global` object.

Global Scope Example

// Global scope - accessible everywhere
var globalVar = "I'm global";
let globalLet = "Also global";
const globalConst = "Also global";

function myFunction() {
  console.log(globalVar); // "I'm global" ✓
  console.log(globalLet); // "Also global" ✓
}

myFunction();
console.log(globalVar); // "I'm global" ✓

// In browser:
console.log(window.globalVar); // "I'm global" ✓

// In Node.js:
console.log(global.globalVar); // undefined (var at top level acts differently)

Variables declared at the top level are in global scope and accessible from any function or block.

2. Function Scope (Local Scope)

Variables declared inside a function are only accessible inside that function. Each function creates its own scope. Variables in function scope are created when the function is called and destroyed when it finishes executing. This is the foundation of data encapsulation in JavaScript.

Function Scope Example

function outer() {
  var functionVar = "I'm in function scope";
  
  function inner() {
    console.log(functionVar); // "I'm in function scope" ✓
    // Can access parent function's variables
  }
  
  inner();
}

outer();
// console.log(functionVar); // ReferenceError! Not accessible here

function test() {
  var x = 10;
  let y = 20;
  const z = 30;
  
  console.log(x, y, z); // 10 20 30 ✓
}

test();
// console.log(x); // ReferenceError!
// console.log(y); // ReferenceError!
// console.log(z); // ReferenceError!

Variables inside functions are scoped to that function and not accessible outside.

3. Block Scope (Introduced in ES6)

Block scope refers to variables declared with `let` and `const` inside a block (anything within curly braces). This includes if statements, loops, switches, and try-catch blocks. Variables declared with `var` do NOT have block scope – they are function-scoped instead. This is a major difference between `var` and `let`/`const`.

Block Scope Example

// if block scope
if (true) {
  let blockLet = "I'm block scoped";
  const blockConst = "Also block scoped";
  var blockVar = "I'm function scoped!";
  
  console.log(blockLet); // "I'm block scoped" ✓
}

// console.log(blockLet); // ReferenceError! Block scope
// console.log(blockConst); // ReferenceError! Block scope
console.log(blockVar); // "I'm function scoped!" ✓ (var ignores blocks)

// for loop block scope
for (let i = 0; i < 3; i++) {
  console.log(i); // 0, 1, 2 ✓
  // i is block-scoped to this iteration
}
// console.log(i); // ReferenceError! (let has block scope)

for (var j = 0; j < 3; j++) {
  // j is function-scoped
}
console.log(j); // 3 ✓ (var leaks out of loop)

let and const respect block scope, but var is function-scoped and ignores block boundaries.

4. Lexical Scope (Static Scope)

Lexical scope means that the accessible variables are determined by the function's position in the source code, not where it's called from. A function can access variables from its parent scope (the scope it was defined in), not from where it's executed. This is the most important concept for understanding closures.

Lexical Scope Example

let globalValue = "Global";

function outer() {
  let outerValue = "Outer";
  
  function inner() {
    let innerValue = "Inner";
    
    // inner can access:
    console.log(innerValue);   // "Inner" ✓ (own scope)
    console.log(outerValue);   // "Outer" ✓ (parent scope - lexical)
    console.log(globalValue);  // "Global" ✓ (grandparent scope)
  }
  
  inner();
  // console.log(innerValue); // ReferenceError! Can't access child scope
}

outer();

// The chain is determined by WHERE functions are DEFINED,
// not WHERE they are CALLED

function callOuter() {
  let callOuterValue = "Call Outer";
  outer(); // outer still accesses outerValue from its definition spot
           // NOT from the callOuterValue here
}

callOuter();

Lexical scope means functions inherit variables from where they're defined, not where they're called.

The Scope Chain Explained

The scope chain is the mechanism JavaScript uses to look up variables. When you reference a variable, JavaScript searches for it in this order:

  1. Current Scope: Is the variable declared in the current scope?
  2. Parent Scope: If not found, check the parent scope (the scope where the function was defined)
  3. Up the Chain: Keep moving up the scope chain until finding the variable
  4. Global Scope: If not found anywhere, check global scope
  5. ReferenceError: If not found anywhere, throw a ReferenceError

Scope Chain Lookup

let globalVar = "Global";

function level1() {
  let level1Var = "Level 1";
  
  function level2() {
    let level2Var = "Level 2";
    
    function level3() {
      let level3Var = "Level 3";
      
      // Variable lookup order:
      console.log(level3Var);  // ✓ Found in current scope (Level 3)
      console.log(level2Var);  // ✓ Found in parent scope (Level 2)
      console.log(level1Var);  // ✓ Found in grandparent scope (Level 1)
      console.log(globalVar);  // ✓ Found in global scope (Global)
      // console.log(unknown); // ✗ ReferenceError! Not found anywhere
    }
    
    level3();
  }
  
  level2();
}

level1();

// Visualization of scope chain:
// Global Scope
//   └── level1() scope
//         └── level2() scope
//               └── level3() scope (searches here first, then up)

JavaScript searches for variables starting from the current scope and moving up through parent scopes.

var vs let vs const Scope Behavior

Understanding scope differences between var, let, and const is crucial for writing correct JavaScript.

var vs let vs const Scoping

// VAR: Function-scoped (not block-scoped)
function varExample() {
  if (true) {
    var x = 10;
  }
  console.log(x); // 10 ✓ (var leaks out of block)
}

// LET: Block-scoped (respects block boundaries)
function letExample() {
  if (true) {
    let y = 20;
  }
  // console.log(y); // ReferenceError! (let respects blocks)
}

// CONST: Block-scoped (like let)
function constExample() {
  if (true) {
    const z = 30;
  }
  // console.log(z); // ReferenceError! (const respects blocks)
}

// In loops - major difference
function loopVar() {
  for (var i = 0; i < 3; i++) { }
  console.log(i); // 3 ✓ (var loops leak)
}

function loopLet() {
  for (let j = 0; j < 3; j++) { }
  // console.log(j); // ReferenceError! (let is block-scoped)
}

// Rule: Use const by default, let when you need to reassign, avoid var

var is function-scoped, while let and const are block-scoped. This difference matters significantly in loops and conditionals.

Closures and Scope

Closures are created by the scope chain. When a function is returned or passed as a callback, it carries its scope chain with it, "closing over" the variables it references. This is one of JavaScript's most powerful features.

Closures Demonstrate Scope

function createCounter() {
  let count = 0; // This variable is captured in the closure
  
  return function increment() {
    count += 1; // Accesses 'count' from parent scope
    return count;
  };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

// 'count' is not accessible here:
// console.log(count); // ReferenceError!

// But the returned function can still access it through closure
// This demonstrates lexical scope and closures working together

// Another closure example:
function makeMultiplier(multiplier) {
  return function(number) {
    return number * multiplier; // Closes over 'multiplier'
  };
}

const double = makeMultiplier(2);
const triple = makeMultiplier(3);

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

// Each closure has its own 'multiplier' variable

Functions retain access to their parent scope even after returning, creating closures that capture variables.

Common Scope Pitfalls

❌ Pitfall 1: Using var in Loops

The Classic Loop Closure Bug

// WRONG: Using var in loop
const functions = [];
for (var i = 0; i < 3; i++) {
  functions.push(function() {
    console.log(i);
  });
}

functions[0](); // 3 (Oops! Expected 0, got 3)
functions[1](); // 3 (Oops! Expected 1, got 3)
functions[2](); // 3 (Oops! Expected 2, got 3)

// WHY: var is function-scoped, so there's only ONE i variable
// By the time functions are called, i = 3

// CORRECT: Use let (block-scoped)
const functions2 = [];
for (let i = 0; i < 3; i++) {
  functions2.push(function() {
    console.log(i);
  });
}

functions2[0](); // 0 ✓
functions2[1](); // 1 ✓
functions2[2](); // 2 ✓

// WHY: let creates a NEW i for each iteration

Using var in loops is a classic bug. Use let instead to get a new variable for each iteration.

❌ Pitfall 2: Accessing Variables Before Declaration

Hoisting and Temporal Dead Zone

// With var (hoisted, initialized to undefined):
console.log(x); // undefined (not ReferenceError)
var x = 10;

// With let/const (hoisted but not initialized - Temporal Dead Zone):
// console.log(y); // ReferenceError! (Temporal Dead Zone)
let y = 10; // y is only accessible from here

// Better practice: Always declare before using
let z = 10;
console.log(z); // 10 ✓

// If you need to check existence, use typeof for var:
console.log(typeof undeclared); // "undefined" (safe check)
// But better to declare everything you use

Variables with let/const are in a 'Temporal Dead Zone' until their declaration is reached.

❌ Pitfall 3: Global Scope Pollution

Accidentally Creating Global Variables

// WRONG: Missing var/let/const creates global variable
function process() {
  result = calculateSomething(); // Oops! No var/let/const
  return result;
}

function process2() {
  result = calculateSomethingElse(); // Same global variable!
  return result; // Overwrites previous result
}

// CORRECT: Always declare with var/let/const
function processRight() {
  const result = calculateSomething(); // Block-scoped
  return result;
}

function process2Right() {
  const result = calculateSomethingElse(); // Different variable
  return result;
}

// Enable strict mode to prevent accidental globals:
"use strict";

function strictTest() {
  unknownVar = 5; // ReferenceError in strict mode ✓ (catches the bug)
}

Always declare variables with var, let, or const to avoid polluting global scope and creating bugs.

Scope Best Practices

  • Use const by default: const prevents reassignment and makes intentions clear
  • Use let when reassignment is needed: let has proper block scope
  • Avoid var: var's function scoping causes confusion and bugs
  • Minimize global scope: Wrap code in functions or modules
  • Use modules: Import/export to control what's visible
  • Use strict mode: "use strict"; at the top prevents many scope bugs
  • Understand closures: They demonstrate scope working perfectly

Interview Q&A About Scope

Q: What's the difference between let and var?

A: var is function-scoped, while let is block-scoped. var is hoisted and initialized to undefined, while let is hoisted but not initialized (Temporal Dead Zone). Use let by default – it prevents many common bugs.

Q: Explain the scope chain.

A: When accessing a variable, JavaScript looks in the current scope first, then the parent scope, up through all parent scopes, and finally global scope. If not found anywhere, it throws a ReferenceError. This creates a chain of accessible scopes based on lexical (static) position in code.

Q: What is lexical scope?

A: Lexical (or static) scope means that accessible variables are determined by the function's position in the source code at definition time, not where it's called from. A function always has access to variables from the scope where it was defined.

Q: Why does this loop code fail?

for (var i = 0; i < 3; i++) { callbacks.push(() => console.log(i)); }

A: var is function-scoped, so there's only one i variable for the entire loop. By the time the callbacks execute, i is 3. Fix: use let instead, which creates a new i for each iteration.

Summary

  • 🎯 Scope determines where variables are accessible
  • 🎯 JavaScript has global, function, block, and lexical scope
  • 🎯 The scope chain determines variable lookup
  • 🎯 Lexical scope means access is determined by definition, not execution
  • 🎯 Use const, then let – avoid var entirely
  • 🎯 Closures work because of scope and the scope chain
  • 🎯 Understanding scope is essential for writing correct JavaScript