JavaScript Interview Prep
Scope & Closures

Scope Chain

The Ladder JavaScript Climbs to Find Your Variables

LinkedIn Hook

When you type console.log(x), JavaScript doesn't just "know" what x is. It runs a lookup — and that lookup follows a very specific ladder called the scope chain.

Current scope -> parent -> grandparent -> ... -> global. The first match wins. Miss them all, and you get a ReferenceError.

This one mechanic explains variable shadowing (why inner x wins over outer x), why a var x = 20 inside bar() has zero effect on a foo() defined in the global scope, and why ReferenceError means "I checked every step and found nothing."

In this lesson you'll learn exactly how the scope chain is built, how shadowing works, and the tricky lexical-vs-call-site question that trips up most candidates.

Read the full lesson -> [link]

#JavaScript #InterviewPrep #ScopeChain #LexicalScope #Frontend #CodingInterview #WebDevelopment


Scope Chain thumbnail


What You'll Learn

  • How JavaScript walks the scope chain to resolve a variable reference
  • What variable shadowing is and how the "closest match" rule works
  • Why the scope chain follows where a function was defined, never where it was called

The Library-Bookshelf Model

When JavaScript encounters a variable, it doesn't just look in the current scope and give up. It walks up a chain — the scope chain — checking each parent scope until it finds the variable or reaches the global scope.

Imagine you're looking for a specific book. First you check your desk. Not there? Check your room's bookshelf. Still not there? Check the living room shelf. Still not there? Check the library (global scope). If even the library doesn't have it — ReferenceError.

How the Scope Chain Works

let a = "global a";

function outer() {
  let b = "outer b";

  function middle() {
    let c = "middle c";

    function inner() {
      let d = "inner d";

      // Scope chain lookup:
      console.log(d); // Found in inner's own scope
      console.log(c); // Not in inner -> found in middle
      console.log(b); // Not in inner, not in middle -> found in outer
      console.log(a); // Not in inner, middle, or outer -> found in global
    }

    inner();
  }

  middle();
}

outer();

The scope chain for inner() is:

inner -> middle -> outer -> global

Each function has a hidden [[Scope]] property (sometimes called [[Environment]]) that points to its outer lexical environment. This chain is set when the function is created.

Variable Shadowing

When a variable in an inner scope has the same name as one in an outer scope, the inner one shadows the outer one:

let x = "global";

function outer() {
  let x = "outer"; // shadows global x

  function inner() {
    let x = "inner"; // shadows outer x
    console.log(x);  // "inner"
  }

  inner();
  console.log(x); // "outer"
}

outer();
console.log(x); // "global"

Each scope has its own x. The scope chain finds the closest one first and stops looking.

Scope Chain with var, let, and const

function demo() {
  var a = 1;  // function-scoped
  let b = 2;  // block-scoped

  if (true) {
    var c = 3;  // still function-scoped (leaks out of if block)
    let d = 4;  // block-scoped (stays in if block)
    console.log(a, b, c, d); // 1 2 3 4 -- all accessible
  }

  console.log(a); // 1  -- own scope
  console.log(b); // 2  -- own scope
  console.log(c); // 3  -- var leaked out of the if block!
  // console.log(d); // ReferenceError -- let stayed in the block
}

demo();

Tricky Example — Scope Chain Resolution

var x = 10;

function foo() {
  console.log(x); // What does this print?
}

function bar() {
  var x = 20;
  foo();
}

bar(); // Output: 10

Why 10? Because foo was defined in the global scope. Its scope chain is foo -> global. It doesn't matter that bar called foo — the scope chain follows where the function was written, not where it was called.

Scope Chain visual 1


Common Mistakes

  • Thinking a variable is "found" at the call site's scope. The scope chain is built from where the function was defined, so foo() in the example above will never see bar's local x.
  • Forgetting that the scope chain stops at the first match. Once an inner x is found, outer xs are invisible (shadowing) — even accidental shadowing silently changes behavior.
  • Assuming var in an if or for block creates a new rung on the scope chain. It doesn't — var only respects function boundaries, so block-level vars live on the enclosing function's rung.

Interview Questions

Q: What is the scope chain in JavaScript?

The scope chain is the ordered list of lexical environments that JavaScript walks when resolving a variable. It starts at the current scope, moves to the enclosing function's scope, then to that function's enclosing scope, and so on up to the global scope. The first match wins; if nothing matches, you get a ReferenceError.

Q: What is variable shadowing?

Shadowing is when an inner scope declares a variable with the same name as one in an outer scope. Inside the inner scope, references to that name resolve to the inner variable — the outer one is "hidden" (shadowed) and cannot be accessed unless you leave the inner scope.

Q: In the code below, what does foo() log and why?

var x = 1;
function foo() { console.log(x); }
function bar() { var x = 2; foo(); }
bar();

It logs 1. foo() was defined in the global scope, so its scope chain goes foo -> global. It doesn't look at bar's local scope because JavaScript uses lexical scope, not dynamic scope. The scope chain is determined at write time.

Q: What happens when a variable isn't found anywhere in the scope chain?

JavaScript throws a ReferenceError: x is not defined (in strict mode or let/const). In sloppy-mode assignments like y = 5 without a prior declaration, the engine silently creates a property on the global object — a common bug source, which is one reason strict mode exists.


Quick Reference — Cheat Sheet

SCOPE CHAIN -- QUICK MAP

Lookup order (always outward):
  current scope -> parent -> grandparent -> ... -> global

Rules:
  - First match wins (shadowing)
  - Not found anywhere -> ReferenceError
  - Chain is built at DEFINITION time, not call time
  - Each function has a hidden [[Scope]] / [[Environment]] slot

Scope boundaries:
  var        -> function boundaries only
  let/const  -> every {} block

Previous: Lexical Scope -> Where You Write a Function Decides What It Can See Next: Closures -> The Variables a Function Never Forgets


This is Lesson 2.2 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.

On this page