Skip to main content

Command Palette

Search for a command to run...

JavaScript Lexical Scope and Compilation

Updated
6 min read

Part 2 of the JavaScript Deep Dive Series

Before JavaScript executes a single line of your code, something crucial happens behind the scenes. While most developers think JavaScript executes and runs code line by line(I know I mentioned this in the first series), the reality is more sophisticated and understanding this process is the key to mastering scope, hoisting, and variable behavior. Javascript still executes code line by line, but before that it interprets/compiles the javascript code.
When you write a javascript code, the first thing that happens is what I refer to as “Parsing” also can be referred to as “Compilation” or interpretation.

A relatable example is when you have a syntax error in your code on line 20, but immediately you recompile, you get an error, when infact your code hasn’t executed up to line 20, the reason why this error shows up immediately is because of the first step “Parsing/Compilation”, during this phase, Javascript engines runs through the code, define variables, verify syntaxes, etc.

So, your code is first compiled, then executed, and that brings us to “The Two step process”.

JavaScript's Two-Step Process

Javascript follows a two-step process:

  1. Compilation Phase - JavaScript scans through your entire code, setting up scope and preparing for execution

  2. Execution Phase - The actual running of your code, line by line

This compilation step is where all the "magic" of hoisting, scope creation, and variable declarations happens.

Definitions

Before we proceed into JavaScript's compilation process, let's explain a few terms you will come accross:

Scope - The accessibility of variables and functions in different parts of your code. It determines where you can use a particular variable.

Lexical Scope - Scope that is determined by where variables and functions are declared in your code (at write-time), not where they are called from (at runtime).

Global Scope - The outermost scope in JavaScript. Variables declared here are accessible from anywhere in your program(more like in the window).

Function Scope - A scope created inside a function. Variables declared here are only accessible within that function.

Block Scope - A scope created by curly braces {} when used with let or const. Variables declared here are only accessible within that block.

Source Reference - When a variable is being read or accessed (appears on the right-hand side of an assignment or in expressions like console.log(variable)).

Target Reference - When a variable is being assigned a value (appears on the left-hand side of an assignment like variable = "value").

Compilation Phase - The first step where JavaScript scans through your code, sets up scope, and prepares variable declarations before any code executes.

Execution Phase - The second step where JavaScript actually runs your code line by line.

Scope Chain - The hierarchy of scopes that JavaScript searches through when looking up a variable, starting from the innermost scope and moving outward.

Hoisting - The behavior where variable and function declarations are processed during the compilation phase, making them available throughout their scope even before their declaration line is reached during execution.

Step 1: Compilation - The Setup Phase

During compilation, JavaScript's engine acts like an organizer, scanning through your code and asking two critical questions about every variable it encounters:

"What scope does this belong to?"
"Is this a source or target reference?"

var teacher = "Kyle";

function otherClass() {
    teacher = "Suzy";
    topic = "React";
    console.log("Welcome!");
}

otherClass();

Let's trace through the compilation phase:

Line 1: var teacher = "Kyle"

  • Compiler finds teacher declaration

  • Creates teacher in global scope

  • Notes: This will be a target reference (receiving a value)

Line 3: function otherClass()

  • Compiler creates otherClass function in global scope

  • Creates new scope for inside the function

Line 4: teacher = "Suzy"

  • Compiler sees teacher reference

  • No declaration here, so it's a source reference (looking up existing variable)

  • Notes: Will look up scope chain during execution

Line 5: topic = "React"

  • Compiler sees topic reference

  • No declaration anywhere - this will create an implicit global

Source vs Target References

Understanding the difference between source and target references is crucial:

Target Reference = Left-hand side of assignment (receiving a value)

var student = "Frank";    // student is TARGET
teacher = "Kyle";         // teacher is TARGET

Source Reference = Right-hand side of assignment (providing a value)

console.log(teacher);     // teacher is SOURCE
var message = teacher;    // teacher is SOURCE, message is TARGET

Why This Matters

The engine handles source and target references completely differently:

Target references: Must have a declared variable to assign to Source references: Must find an existing variable to read from

// Compilation phase creates these declarations
var teacher;              // TARGET reference prepared
var student;              // TARGET reference prepared

// Execution phase
teacher = "Kyle";         // TARGET - assigns to existing declaration
console.log(teacher);     // SOURCE - looks up existing value
topic = "React";          // TARGET - no declaration found, creates global
console.log(subject);     // SOURCE - no declaration found, ReferenceError!

Lexical Scope - Where You Write Matters

Lexical scope means scope is determined by where you write your code, not where you call it. The physical placement of your variables and functions determines their scope relationships.

var teacher = "Kyle";

function otherClass() {
    var teacher = "Suzy";

    function ask() {
        console.log(teacher);  // Which teacher?
    }

    ask();
}

otherClass(); // "Suzy"

The ask() function looks for teacher in this order:

  1. Local scope (inside ask) - not found

  2. Enclosing scope (inside otherClass) - found "Suzy"!

  3. Global scope - never reaches here

This lookup happens at compile time based on where you wrote the code, not where you called it.

Scope Chain Resolution

JavaScript creates a scope chain during compilation - a linked list of scopes from innermost to outermost:

var global = "I'm global";

function outer() {
    var outerVar = "I'm in outer";

    function inner() {
        var innerVar = "I'm in inner";
        console.log(innerVar);  // Found in inner scope
        console.log(outerVar);  // Found in outer scope  
        console.log(global);    // Found in global scope
        console.log(missing);   // ReferenceError!
    }

    inner();
}

outer();

Scope chain for inner() function: Inner Scope → Outer Scope → Global Scope

The engine walks up this chain until it finds the variable or reaches the end (ReferenceError).

The Compilation Process in Action

Let's trace through a complete example:

console.log(teacher);        // What happens here?

var teacher = "Kyle";

function ask() {
    console.log(teacher);
}

ask();

Compilation Phase:

  1. Line 3: Create teacher declaration in global scope

  2. Line 5: Create ask function in global scope

  3. Line 6: Create new scope for ask function

  4. All source references noted for execution phase

Execution Phase:

  1. Line 1: console.log(teacher) - SOURCE reference to teacher

    • teacher exists (from compilation) but value is undefined

    • Output: undefined

  2. Line 3: teacher = "Kyle" - TARGET reference

    • Assigns "Kyle" to existing teacher declaration
  3. Line 8: ask() - Calls function

  4. Line 6: console.log(teacher) - SOURCE reference

    • Looks up teacher in scope chain, finds "Kyle"

    • Output: "Kyle"

Common Scope Misconceptions

"Hoisting moves declarations to the top" False. Nothing physically moves. Compilation creates declarations before execution begins.

"let and const aren't hoisted"
False. They are hoisted but in a "temporal dead zone" until their line is reached.

"Functions create scope" Partially true. Function declarations create scope, but so do blocks with let/const.

"Scope is determined at runtime" False. Lexical scope is determined at compile time by where you write code.

Practical Applications

Understanding compilation and lexical scope helps you:

Debug Variable Access Issues

function broken() {
    console.log(name);  // ReferenceError - not undefined!
    let name = "Frank";
}

Predict Hoisting Behavior

console.log(typeof teacher);  // "undefined" (var hoisting)
console.log(typeof student);  // ReferenceError (let in TDZ)

var teacher = "Kyle";
let student = "Frank";

Understand Closure Creation

function makeCounter() {
    var count = 0;
    return function() {
        return ++count;  // Lexical scope preserves access to count
    };
}

Javascript Deep dive

Part 3 of 4

A journey through JavaScript's inner workings, one concept at a time. This series goes beyond syntax and into the heart of how JavaScript actually works. We'll explore the engine, the call stack, memory management, and other advanced concepts.

Up next

JavaScript's Thread of Execution - How Code Really Runs

Part 1 of the JavaScript Deep Dive Series Welcome to JavaScript Deep Dive - a comprehensive series where we'll journey through JavaScript's inner workings, one concept at a time. Over the next several articles, we'll explore everything from basic exe...

More from this blog

C

Cracked Chefs by Oluwaferanmi Adeniji

19 posts

Battle-tested Coding patterns, Javascript wizardry, System Design, Product Engineering and Management, and architectural secrets.