Back to course

Scope and Lexical Environment

Content Reader1 words • 0:00 • Browser TTS

image.png

Scope and Lexical Environment – The Neighborhood Rules of JavaScript

Imagine you live in a gated community with multiple neighborhoods, each with its own rules about who can access what. Some resources are available to everyone (like the community pool), some are only for your neighborhood (like the neighborhood park), and some are private to your house (like your personal garage).

This is exactly how scope works in JavaScript – it determines who has access to what variables and when.

But here's where it gets interesting: JavaScript doesn't just decide access randomly. It follows a very specific blueprint called the lexical environment – think of it as the architectural plan that determines these neighborhood boundaries before anyone moves in.

What is Scope?

Scope is the accessibility of variables, functions, and objects in your code. It answers the fundamental question: "Can I access this variable from here?"

There are three main types of scope in JavaScript:

1. Global Scope – The Public Plaza 🌍

Variables declared in global scope are like the town square – accessible from anywhere in your program.

// Global scope - everyone can access this
let townSquare = "Welcome everyone!";

function neighborhood1() {
  console.log(townSquare); // "Welcome everyone!"
}

function neighborhood2() {
  console.log(townSquare); // "Welcome everyone!"
}

2. Function Scope – The Private Neighborhood 🏘️

Variables declared inside a function are like gated neighborhoods – only accessible within that function.

function privateNeighborhood() {
  // Function scope - only accessible within this function
  let neighborhoodSecret = "Only neighbors know this!";
  
  console.log(neighborhoodSecret); // Works fine
}

privateNeighborhood();
console.log(neighborhoodSecret); // ReferenceError: neighborhoodSecret is not defined

3. Block Scope – The Individual Houses 🏠

Variables declared with let or const inside blocks (anything between {}) are like private houses – only accessible within that block.

if (true) {
  // Block scope - only accessible within this block
  let houseSecret = "Only house members know this!";
  console.log(houseSecret); // Works fine
}

console.log(houseSecret); // ReferenceError: houseSecret is not defined

image.png

What is Lexical Environment?

While scope tells us what can be accessed, lexical environment is the internal mechanism that makes it possible. Think of lexical environment as the blueprint that JavaScript creates during the creation phase of execution contexts.

The Architecture Blueprint 📋

Every execution context has a lexical environment that contains:

  1. Environment Record: A storage space for variables and functions in the current scope
  2. Reference to Outer Environment: A pointer to the parent scope (like a map to the parent neighborhood)
// Global Lexical Environment
let globalVar = "I'm global";

function outerFunction() {
  // Outer Function Lexical Environment
  let outerVar = "I'm in outer function";
  
  function innerFunction() {
    // Inner Function Lexical Environment
    let innerVar = "I'm in inner function";
    
    // This function can access:
    console.log(innerVar);  // Its own variable
    console.log(outerVar);  // Parent function's variable
    console.log(globalVar); // Global variable
  }
  
  innerFunction();
}

outerFunction();

image.png

The Scope Chain – Following the Breadcrumbs 🍞

When JavaScript looks for a variable, it follows the scope chain – like following breadcrumbs from your current location back to the town square.

The Search Process 🔍

  1. Start Local: Look in the current lexical environment
  2. Go Up One Level: If not found, check the outer (parent) environment
  3. Keep Going: Continue up the chain until reaching global scope
  4. Give Up: If still not found, throw a ReferenceError
let level1 = "Global level";

function level2() {
  let level2Var = "Function level";
  
  function level3() {
    let level3Var = "Block level";
    
    // JavaScript searches in this order:
    console.log(level3Var); // 1. Found in current scope ✅
    console.log(level2Var); // 2. Found in parent scope ✅
    console.log(level1);    // 3. Found in global scope ✅
    console.log(level4Var); // 4. Not found anywhere ❌ ReferenceError
  }
  
  level3();
}

level2();

Lexical vs Dynamic Scoping 📍

JavaScript uses lexical scoping (also called static scoping), which means the scope is determined by where variables are declared in the code, not where they are called from.

The Neighborhood Map is Fixed 🗺️

let message = "Global message";

function outer() {
  let message = "Outer message";
  
  function inner() {
    console.log(message); // What gets printed?
  }
  
  return inner;
}

function somewhere() {
  let message = "Somewhere message";
  let innerFunc = outer();
  innerFunc(); // "Outer message" - not "Somewhere message"!
}

somewhere();

Why "Outer message"? Because inner() was defined inside outer(), so it has access to outer()'s lexical environment, regardless of where it's called from.

image.png

Variable Shadowing – When Names Collide 👥

When variables in different scopes have the same name, the inner scope "shadows" (hides) the outer scope variable.

let name = "Global Alice";

function outer() {
  let name = "Outer Bob";
  
  function inner() {
    let name = "Inner Charlie";
    console.log(name); // "Inner Charlie"
    
    // The global and outer 'name' variables are shadowed
    // They still exist, but are inaccessible from here
  }
  
  inner();
  console.log(name); // "Outer Bob"
}

outer();
console.log(name); // "Global Alice"

The Window Trick (Only in Browsers) 🪟

In browsers, you can access shadowed global variables using the window object:

let color = "Global Blue";

function paint() {
  let color = "Local Red";
  
  console.log(color);        // "Local Red"
  console.log(window.color); // "Global Blue" (browser only)
}

paint();

Practical Examples – Scope in the Wild

Example 1: The Counter Dilemma 🔢

// Problem: This doesn't work as expected
for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // Prints: 3, 3, 3
  }, 100);
}

// Solution 1: Use let (block scope)
for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i); // Prints: 0, 1, 2
  }, 100);
}

// Solution 2: Create a closure
for (var i = 0; i < 3; i++) {
  (function(j) {
    setTimeout(() => {
      console.log(j); // Prints: 0, 1, 2
    }, 100);
  })(i);
}

Example 2: The Module Pattern 📦

const counter = (function() {
  // Private variables (not accessible outside)
  let count = 0;
  
  // Return public interface
  return {
    increment: function() {
      count++;
      return count;
    },
    decrement: function() {
      count--;
      return count;
    },
    getCount: function() {
      return count;
    }
  };
})();

console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
console.log(count); // ReferenceError: count is not defined

Example 3: Event Handler Gotcha 🎯

// Problem: All buttons alert the same value
const buttons = document.querySelectorAll('button');

for (var i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener('click', function() {
    alert('Button ' + i + ' clicked'); // Always shows last value of i
  });
}

// Solution: Use let or create proper closure
for (let i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener('click', function() {
    alert('Button ' + i + ' clicked'); // Shows correct value
  });
}

Understanding Closures Through Scope 🔐

A closure is when an inner function has access to variables from its outer scope even after the outer function has finished executing.

function createGreeting(greeting) {
  // This function creates a lexical environment
  
  return function(name) {
    // This inner function has access to 'greeting'
    // even after createGreeting() finishes
    console.log(greeting + ', ' + name + '!');
  };
}

const sayHello = createGreeting('Hello');
const sayHi = createGreeting('Hi');

sayHello('Alice'); // "Hello, Alice!"
sayHi('Bob');      // "Hi, Bob!"

// 'greeting' variables are still accessible through closures!

image.png

Best Practices for Scope Management

1. Minimize Global Variables 🌍

// Avoid this
var userName = 'Alice';
var userAge = 25;
var userEmail = 'alice@example.com';

// Prefer this
const user = {
  name: 'Alice',
  age: 25,
  email: 'alice@example.com'
};

2. Use let and const Instead of var 📝

// Avoid var (function-scoped)
for (var i = 0; i < 3; i++) {
  // var i is accessible outside the loop
}
console.log(i); // 3

// Prefer let (block-scoped)
for (let j = 0; j < 3; j++) {
  // let j is only accessible within the loop
}
console.log(j); // ReferenceError

3. Create Modules for Organization 📦

// User module
const UserModule = (function() {
  // Private variables
  let users = [];
  
  // Public API
  return {
    addUser: function(user) {
      users.push(user);
    },
    getUsers: function() {
      return [...users]; // Return a copy
    },
    getUserCount: function() {
      return users.length;
    }
  };
})();

Common Interview Questions & Answers

Q1: What will this code output?

function test() {
  console.log(a); // ?
  console.log(b); // ?
  
  var a = 1;
  let b = 2;
}
test();

Answer: undefined and ReferenceError. var a is hoisted and initialized with undefined, but let b is in the temporal dead zone.

Q2: Explain this closure behavior:

const funcs = [];
for (var i = 0; i < 3; i++) {
  funcs[i] = function() {
    return i;
  };
}
console.log(funcs[0]()); // ?

Answer: 3. All functions share the same lexical environment and reference the same i variable, which is 3 after the loop completes.

Q3: How would you fix the above code?

// Solution 1: Use let
for (let i = 0; i < 3; i++) {
  funcs[i] = function() {
    return i;
  };
}

// Solution 2: Use closure
for (var i = 0; i < 3; i++) {
  funcs[i] = (function(index) {
    return function() {
      return index;
    };
  })(i);
}

Summary

Scope

  • Global Scope: Accessible everywhere (town square)
  • Function Scope: Accessible only within the function (private neighborhood)
  • Block Scope: Accessible only within the block (individual house)

Lexical Environment

  • Environment Record: Storage for variables in current scope
  • Outer Reference: Pointer to parent scope
  • Scope Chain: The path JavaScript follows to find variables

Key Principles

  • Lexical Scoping: Scope is determined by where variables are declared, not where they're called
  • Scope Chain: JavaScript searches from inner to outer scope
  • Closures: Inner functions retain access to outer scope variables
  • Variable Shadowing: Inner scope variables hide outer scope variables with the same name

My Personal Insight

Understanding scope and lexical environments was like getting a map to JavaScript's neighborhood system. Once I realized that JavaScript doesn't randomly decide what variables you can access – it follows a very logical blueprint created during the compilation phase – debugging became so much easier.

The "where it's written" rule (lexical scoping) vs "where it's called" rule (dynamic scoping) was the biggest lightbulb moment for me. JavaScript cares about the structure of your code, not the execution flow.

Next Up

Now that you understand how JavaScript manages variable access through scope and lexical environments, we'll dive into Variables and Data Types – exploring the different ways to declare variables and the types of data JavaScript can work with.

Remember: Scope isn't just a rule – it's the foundation that makes closures, modules, and clean code architecture possible! 🏗️

Photo of Rahul Aher

Written by Rahul Aher

I'm Rahul, Sr. Software Engineer (SDE II) and passionate content creator. Sharing my expertise in software development to assist learners.

More about me