Course outline · 0% complete

0/27 lessons0%

Course overview →

Scope: what a function can see

lesson 1-1 · ~10 min · 1/27

Welcome back

In JavaScript for Beginners you wrote functions, loops, arrays, and objects. This course is about how JavaScript actually works underneath, which is the material interviews are made of.

Scope is the right place to start, for two reasons.

Half the bugs you will debug at work come down to which variable a line of code is actually touching. And closures, the topic of the next lesson, are nothing but these scope rules applied carefully.

Closures are the mechanism behind every event handler, every debounce, and every React hook, so the payoff for getting scope precise is immediate.

First term: scope, the set of variables a piece of code can see.

JavaScript uses lexical scope, which means what a function can see is decided by where it is written in the source code, not by where it is called from. That distinction is the one to hold on to, because it is what makes scope predictable by reading rather than by tracing.

A function written inside another function can read the outer function's variables.

function outer() {
  const secret = "level 1";
  function inner() {
    console.log(secret);
  }
  inner();
}

Reading a variable from the enclosing function

inner never declares secret, and it prints it anyway.

function outer() {
  const secret = "level 1";
  function inner() {
    console.log(secret);
  }
  inner();
}
outer();

Output

level 1

inner reads secret from the scope it was written inside, which is what lexical scope means in practice.

Nothing was passed in. There is no parameter and no argument, and the connection is purely a matter of where the function body sits in the file.

The lookup happens when the line runs, not when the function is defined. If secret were reassigned before inner() was called, the new value would print.

Deleting const secret would not make this a silent bug either. The lookup would continue outward to global scope, find nothing, and throw a ReferenceError, which is a loud and useful failure.

The scope chain

When JavaScript needs a variable, it looks in the current function first. If it is not there, it steps outward to the enclosing function, then outward again, all the way to global scope.

That path is the scope chain, and it is fixed the moment the code is written.

Two rules are worth memorizing.

  • Lookup only goes outward, never inward. outer cannot read variables declared inside inner.
  • If two scopes declare the same name, the closest one wins, which is called shadowing.

The chain ends at global scope, and reaching the end without a match is a ReferenceError rather than undefined. That is a distinction interviewers probe, since an undeclared variable and a declared-but-unassigned one fail differently.

Blocks count as scopes too for let and const. A pair of braces in an if or a for adds a link to the chain, which is a difference from var that lesson 10-1 returns to.

Shadowing

Two different variables share one name.

const name = "global";

function greet() {
  const name = "local";
  console.log(name);
}

greet();
console.log(name);

Output

local
global

Inside greet the closest declaration wins, so the same word refers to different variables on different lines.

The outer variable is untouched, and that is the important half. Shadowing hides a name rather than overwriting it, so the second console.log still prints "global".

There is no way to reach the shadowed outer name from inside greet. The inner declaration blocks the chain for that name entirely.

That is why shadowing shows up in real bugs. A function parameter named the same as an outer variable silently intercepts every reference, and the code reads as though it is using the outer one.

global scopeouter() · const secretinner()needs secret? not here…look one scope out
Scopes nest like boxes. Variable lookup starts in the innermost box and steps outward until it finds the name.

No. Lookup only goes outward, so outer scopes never see inner variables.

The scope chain is one-directional. inner can see everything in outer, and outer can see nothing declared inside inner.

Attempting it throws a ReferenceError rather than giving undefined, since temp does not exist anywhere on the chain from outer outward.

Switching to var does not change the answer. var hoists temp to the top of inner, and function scope is still the boundary, so it never escapes.

To get a value out of inner, it has to leave deliberately, by being returned, by being assigned to something already in an outer scope, or by being passed to a callback. That deliberate escape is the whole subject of the next lesson.

Reaching through three levels

Each function contributes one variable, and the innermost reads all three.

function level1() {
  const a = "a";
  function level2() {
    const b = "b";
    function level3() {
      const c = "c";
      console.log(a + " " + b + " " + c);
    }
    level3();
  }
  level2();
}
level1();

Output

a b c

level3 reads a and b directly, with no help from level2, because the scope chain passes straight through both enclosing functions.

There is no depth limit and no cost to declare. A name resolves at whatever level it is found, and level2 does not have to forward anything.

A template literal would read better here, since ` ${a} ${b} ${c} ` avoids the string-concatenation noise while resolving the same three names.

Reversing the direction fails, which is the check worth running mentally. level1 cannot read c, and adding console.log(c) to it throws a ReferenceError.