Skip to main content
Chapters 1 and 2 laid down a concrete definition of lexical scope (and its parts) and illustrated helpful metaphors for its conceptual foundation. Before proceeding with this chapter, find someone else to explain (written or aloud), in your own words, what lexical scope is and why it’s useful to understand. That seems like a step you might skip, but I’ve found it really does help to take the time to reformulate these ideas as explanations to others. That helps our brains digest what we’re learning!

Understanding the Scope Chain

The connections between scopes that are nested within other scopes is called the scope chain, which determines the path along which variables can be accessed. The chain is directed, meaning the lookup moves upward/outward only.

”Lookup” Is (Mostly) Conceptual

In the previous chapter, we described the runtime access of a variable as a “lookup,” where the Engine has to start by asking the current scope’s Scope Manager if it knows about an identifier/variable, and proceeding upward/outward back through the chain of nested scopes (toward the global scope) until found, if ever. The lookup stops as soon as the first matching named declaration in a scope bucket is found. This suggestion of a runtime lookup process works well for conceptual understanding, but it’s not actually how things usually work in practice.
The color of a marble’s bucket (aka, meta information of what scope a variable originates from) is usually determined during the initial compilation processing. Because lexical scope is pretty much finalized at that point, a marble’s color will not change based on anything that can happen later during runtime.
Since the marble’s color is known from compilation, and it’s immutable, this information would likely be stored with (or at least accessible from) each variable’s entry in the AST; that information is then used explicitly by the executable instructions that constitute the program’s runtime. In other words, Engine doesn’t need to lookup through a bunch of scopes to figure out which scope bucket a variable comes from. That information is already known! Avoiding the need for a runtime lookup is a key optimization benefit of lexical scope. However, this lookup would only be needed once per variable at most, since nothing else during runtime could later change that marble’s color.

Shadowing

“Shadowing” might sound mysterious and a little bit sketchy. But don’t worry, it’s completely legit! Where having different lexical scope buckets starts to matter more is when you have two or more variables, each in different scopes, with the same lexical names. A single scope cannot have two or more variables with the same name; such multiple references would be assumed as just one variable. Consider:
var studentName = "Suzy";

function printStudent(studentName) {
    studentName = studentName.toUpperCase();
    console.log(studentName);
}

printStudent("Frank");
// FRANK

printStudent(studentName);
// SUZY

console.log(studentName);
// Suzy
Before you move on, take some time to analyze this code using the various techniques/metaphors we’ve covered in the book. In particular, make sure to identify the marble/bubble colors in this snippet. It’s good practice!
The studentName variable on line 1 creates a RED(1) marble. The same named variable is declared as a BLUE(2) marble on line 3, the parameter in the printStudent(..) function definition. What color marble will studentName be in the assignment statement and the console.log(studentName) statement? All three studentName references will be BLUE(2). This is a key aspect of lexical scope behavior, called shadowing. The BLUE(2) studentName variable (parameter) shadows the RED(1) studentName. The parameter is shadowing the (shadowed) global variable.

Global Unshadowing Trick

Leveraging the technique I’m about to describe is not very good practice, as it’s limited in utility, confusing for readers of your code, and likely to invite bugs to your program. I’m covering it only because you may run across this behavior in existing programs.
It is possible to access a global variable from a scope where that variable has been shadowed, but not through a typical lexical identifier reference. In the global scope, var declarations and function declarations also expose themselves as properties on the global object. If you’ve written JS for a browser environment, you probably recognize the global object as window. Consider this program:
var studentName = "Suzy";

function printStudent(studentName) {
    console.log(studentName);
    console.log(window.studentName);
}

printStudent("Frank");
// "Frank"
// "Suzy"
The window.studentName expression accesses the global variable studentName as a property on window. That’s the only way to access a shadowed variable from inside a scope where the shadowing variable is present.
Just because you can doesn’t mean you should. Don’t shadow a global variable that you need to access, and conversely, avoid using this trick to access a global variable that you’ve shadowed.
This “trick” only works for accessing a global scope variable, and only one that was declared with var or function. Other forms of global scope declarations do not create mirrored global object properties:
var one = 1;
let notOne = 2;
const notTwo = 3;
class notThree {}

console.log(window.one);       // 1
console.log(window.notOne);    // undefined
console.log(window.notTwo);    // undefined
console.log(window.notThree);  // undefined

Copying Is Not Accessing

Consider:
var special = 42;

function lookingFor(special) {
    var another = {
        special: special
    };

    function keepLooking() {
        var special = 3.141592;
        console.log(special);
        console.log(another.special);  // Ooo, tricky!
        console.log(window.special);
    }

    keepLooking();
}

lookingFor(112358132134);
// 3.141592
// 112358132134
// 42
The special: special is copying the value of the special parameter variable into another container (a property of the same name). Of course, if you put a value in another container, shadowing no longer applies. But that doesn’t mean we’re accessing the parameter special; it means we’re accessing the copy of the value it had at that moment.

Illegal Shadowing

Not all combinations of declaration shadowing are allowed. let can shadow var, but var cannot shadow let:
function something() {
    var special = "JavaScript";

    {
        let special = 42;   // totally fine shadowing
        // ..
    }
}

function another() {
    // ..
    {
        let special = "JavaScript";

        {
            var special = "JavaScript";
            // ^^^ Syntax Error
            // ..
        }
    }
}
The real reason it’s raised as a SyntaxError is because the var is basically trying to “cross the boundary” of (or hop over) the let declaration of the same name, which is not allowed.
Summary: let (in an inner scope) can always shadow an outer scope’s var. var (in an inner scope) can only shadow an outer scope’s let if there is a function boundary in between.

Function Name Scope

A function declaration looks like this:
function askQuestion() {
    // ..
}
And such a function declaration will create an identifier in the enclosing scope named askQuestion. What about this program?
var askQuestion = function(){
    // ..
};
One major difference between function declarations and function expressions is what happens to the name identifier of the function. Consider a named function expression:
var askQuestion = function ofTheTeacher(){
    // ..
};
ofTheTeacher is declared as an identifier inside the function itself:
var askQuestion = function ofTheTeacher() {
    console.log(ofTheTeacher);
};

askQuestion();
// function ofTheTeacher()...

console.log(ofTheTeacher);
// ReferenceError: ofTheTeacher is not defined
Not only is ofTheTeacher declared inside the function rather than outside, but it’s also defined as read-only:
var askQuestion = function ofTheTeacher() {
    "use strict";
    ofTheTeacher = 42;   // TypeError
    //..
};

askQuestion();
// TypeError

Arrow Functions

ES6 added an additional function expression form to the language, called “arrow functions”:
var askQuestion = () => {
    // ..
};
The => arrow function doesn’t require the word function to define it. Arrow functions are lexically anonymous, meaning they have no directly related identifier that references the function.
var askQuestion = () => {
    // ..
};

askQuestion.name;   // askQuestion
Other than being anonymous (and having no declarative form), => arrow functions have the same lexical scope rules as function functions do. An arrow function, with or without { .. } around its body, still creates a separate, inner nested bucket of scope.

Backing Out

When a function (declaration or expression) is defined, a new scope is created. The positioning of scopes nested inside one another creates a natural scope hierarchy throughout the program, called the scope chain. The scope chain controls variable access, directionally oriented upward and outward. Each new scope offers a clean slate, a space to hold its own set of variables. When a variable name is repeated at different levels of the scope chain, shadowing occurs, which prevents access to the outer variable from that point inward. As we step back out from these finer details, the next chapter shifts focus to the primary scope all JS programs include: the global scope.

Build docs developers (and LLMs) love