Skip to content
Theory · Scope · lesson 5 of 8

Shadowing and lexical scope

Let's go back to the apartment with glass rooms. Sometimes a room holds a box with the same label as a box in the hallway. Whoever looks for a thing by its label checks their own room first and takes the nearer box, never getting as far as the hallway.

Variables work the same way. A name already taken outside can be declared once more in a nested block. Inside the block the name means a new variable, while the outer variable stays as it was:

JavaScript
let level = 1;

if (level > 0) {
    let level = 5;
    console.log(level); // 5
}

console.log(level); // 1

Let's walk through the program line by line:

  1. let level = 1 creates the outer variable.
  2. The condition level > 0 is true, and let level = 5 creates a second variable inside the block.
  3. The first console.log(level) prints the inner variable: 5.
  4. After the } the inner variable is gone, and the second console.log prints the outer one: 1.

Shadowing

The inner let level creates a second variable that lives until the block's closing brace. While the inner variable exists, it hides the outer one, and the name level inside the block means the inner one. This is called shadowing.

The block does not change the outer variable, and after the } the name means the outer one again.

Here the room picture stops working. Through the glass, a room also shows the far box in the hallway. In the program, though, until the block ends the name level means only the inner variable, and that name cannot reach the outer one.

One name twice

Whether a name can be declared a second time depends on where the second declaration stands.

In the same block

Declaring one name twice with let or const in the same block is not allowed. This is a syntax error: Node.js finds it before the program starts and runs not a single line of it.

JavaScript
let x = 1;
let x = 2; // ✗
TEXT
SyntaxError: Identifier 'x' has already been declared

A repeated var with the same name is not an error: after var x = 1; var x = 2; the program goes on, and x holds 2.

In a nested block

In a nested block, even a standalone pair of {}, a second declaration with let or const is allowed: that block has its own scope. Shadowing is not an error, but such code is hard to read. It is better to give the inner variable a different name.

Where the function is written

In our picture a function is a room too, built at a particular spot in the apartment. Its windows show what surrounds the spot where it was built, and wherever it is called from, the view from the windows stays the same.

A function sees the variables of the place where it is written, not of the place it is called from. This rule is called lexical scope: visibility is decided by the program's text, not by the order of calls.

JavaScript
const greeting = "Hello";

function greet() {
    return greeting;
}

function run() {
    const greeting = "Hi";
    function inner() {
        return greeting;
    }
    return greet() + " " + inner();
}

console.log(run()); // Hello Hi

Let's see which greeting each function sees:

  1. greet is written at the top level of the file and sees the outer constant greeting with the value "Hello".
  2. The constant greeting inside run is visible only in the body of run. The text of greet is not part of that body, so calling it from run does not affect the result of greet.
  3. inner is written inside run, so it sees the constant greeting from run with the value "Hi".
  4. run joins both results with a space, and the program prints Hello Hi.

Both functions are called from the same place, the body of run, yet they return different things. Only the place where the function's text is written decides.

Interviewers often check this with a "what does the program print" task like the example above. Look not for the place of the call but for the place where the function is written.

Looking up a name

A name is looked up from the inside out:

  1. in the block where it is used;
  2. in the surrounding blocks and functions;
  3. at the top level of the file.

That is how a function written inside another function sees both that function's variables and the file's variables. The lookup stops at the first match, and that is why shadowing works: inner finds greeting in run and never gets as far as the top level of the file.

Common mistake

The goal is to change the outer variable in a nested block, but the line says let:

JavaScript
let message = "none";
const score = 70;

if (score > 50) {
    let message = "pass"; // ✗
}

console.log(message); // none

There is no error, and the program prints none: let created an inner variable that disappeared at the }. To change the outer variable, write an assignment without let: message = "pass";.

Key points

  • A variable with the same name in a nested block shadows the outer one until the block ends, and the outer one does not change.
  • Declaring one name twice with let or const in the same block is not allowed: it is a SyntaxError.
  • A function sees the variables of the place where it is written, not of the place it was called from.
  • A name is looked up from the inside out, and the lookup stops at the first match.

Subscribe to take the course

The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.

Sign in another way