TLDR - JavaScript Scope: Lexical, Block, and Hoisting Basics Scope defines how and where variables are accessed in JavaScript. JavaScript uses lexical scope , meaning scope is determined by a variable's location in the source code. There are two types of lookups: LHS (assignment) and RHS (value retrieval).
Strict mode enforces cleaner code by throwing errors for undeclared variables. Block scope (with let and const ) restricts variable visibility to code blocks, unlike var , which is function-scoped. Hoisting moves declarations to the top of their scope, but not initializations.
Nested scopes resolve variables from the inside out, allowing shadowing. Scoping issues often appear in job interview questions , especially around loops and closures.
In programming languages, scope refers to the set of rules governing how variables are stored and looked up within a program. JavaScript uses lexical scope , meaning that the location where variables and functions are declared during compile time determines their scope.
Think of scope as a set of nested containers - each block, function, or module represents a container that determines where variables can be accessed. JavaScript engines perform two types of lookups when working with variables, depending on the context:
Happens when a variable is on the left side of an assignment operation. The engine searches for a memory location to store the value. Here, the engine finds or creates a place to store the value 42. Happens when a variable's value is being accessed. The engine retrieves the value of foo to pass to console.log.
JavaScript behaves differently depending on whether strict mode is enabled: In strict mode , referencing a variable that doesn’t exist throws a ReferenceError. In non-strict mode , the engine creates global variables if they are not declared (a potential source of bugs).
Lexical scope refers to how JavaScript determines the scope of a variable based on its location in the source code. Consider this example:
Here, bar logs 2 because it looks at the scope where it was defined (global), not where it was called. If you’ve ever been puzzled by this behavior in JavaScript, you’re not alone - it’s a common "aha!" moment for many developers.
Block scope allows variables to exist only within a specific block (e.g., if, for). With let and const, JavaScript introduced true block scoping: In contrast, var is function-scoped, meaning its scope extends to the entire function:
Hoisting is JavaScript’s behavior of moving declarations to the top of their enclosing scope. However, only the declaration is hoisted - the initialization remains in place. To avoid confusion, always declare variables at the top of their scope or use let/const:
Scopes can be nested. When a variable is accessed, the JavaScript engine starts in the current scope and moves outward:
This example shows how each nested scope “shadows” variables from outer scopes. It’s one of those concepts that’ll feel intuitive once you’ve seen it in action a few times. A common interview question demonstrates issues with scoping in loops:
The problem arises because var creates a single shared scope for the loop. By the time the returned function is called, the value of i has incremented to arr.length, and accessing arr[i] results in undefined. Here’s how to fix it:
Understanding scope is crucial for mastering JavaScript. Whether you’re preparing for interviews or building real-world projects, knowing how scope works will save you from countless bugs and help you write cleaner code. I can’t count how many times understanding these fundamentals has helped me debug a tricky issue.
