JavaScript Scopes: Global, Function, and Lexical Scope
Scope is one of the most fundamental concepts in JavaScript. It defines the visibility and accessibility of variables, functions, and objects in different parts of your code during runtime.
If you don't understand scoping rules, you will run into silent bugs, unexpected variable values, and memory leaks. In frontend engineering interviews, you are guaranteed to face questions testing variable isolation, nesting, and lookup chains.
1. What is Scope?
In simple terms, scope determines where in your code a specific variable is accessible. If a variable is "in scope," you can read or modify it. If it is "out of scope," attempting to access it will throw a ReferenceError.
JavaScript uses scope to achieve:
- Variable Isolation: Preventing name collisions. Two different functions can have variables named
countwithout interfering with each other. - Security & Privacy: Restricting access to internal state or data from outer modules.
- Memory Efficiency: Allowing the engine to garbage collect variables that are no longer accessible.
2. Types of Scope in JavaScript
JavaScript has four primary levels of scope:
A. Global Scope
Any variable declared outside of any function or block is in the global scope. Globally-scoped variables are accessible from anywhere in your program.
const globalVar = "I am global";
function test() {
console.log(globalVar); // Accessible here
}
test();
console.log(globalVar); // Accessible here too[!WARNING] Minimise the use of global variables. They clutter the global namespace, increase the risk of collision, and make code difficult to test and maintain. In the browser, variables declared with
varglobally get attached to thewindowobject (window.myVar), which is particularly risky.
B. Function (Local) Scope
Variables declared inside a function (using var, let, or const) are function-scoped. They can only be accessed within that function's body.
function localScopeTest() {
var local = "I am local";
console.log(local); // Works
}
localScopeTest();
console.log(local); // ReferenceError: local is not definedC. Block Scope
Introduced in ES6 (ES2015), block scope restricts variable access to the block {} in which they are declared. Block scope only applies to variables declared with let and const. Variables declared with var do not respect block scope.
if (true) {
var leaked = "I can escape!";
let trapped = "I am trapped!";
const locked = "I am locked too!";
}
console.log(leaked); // "I can escape!" (var leaks out of block)
console.log(trapped); // ReferenceError: trapped is not defined
console.log(locked); // ReferenceError: locked is not definedD. Module Scope
When writing JavaScript modules (using ESM import/export), variables declared inside a module are scoped strictly to that module. They cannot be accessed by other files unless they are explicitly exported.
// math.js
const privateMultiplier = 2; // Module scope
export function double(num) {
return num * privateMultiplier;
}3. Lexical Scope vs. Dynamic Scope
Scoping behaviors fall into two primary designs: Lexical (Static) Scope and Dynamic Scope.
Lexical Scope (Static Scope)
JavaScript uses Lexical Scope. In lexical scope, variable visibility is determined strictly by the physical location of the code blocks at compile time (when you write the code), not at execution time.
Where you declare a function determines what variables it has access to.
const x = "global";
function outer() {
const x = "local";
inner();
}
function inner() {
console.log(x); // Reads from outer environment where inner was defined!
}
outer();Output:
globalWhy global?
Because inner is defined in the global scope. At compile-time, its outer scope reference is set to the global environment. It does not look at outer's scope just because it was called inside outer().
Dynamic Scope
In dynamic scope, variable visibility is determined by the call stack at runtime. If JavaScript used dynamic scope, inner() would look at where it was called (inside outer), and the output of the example above would be local.
[!NOTE] JavaScript's
thiskeyword behaves dynamically based on invocation, but standard variable scoping is strictly static/lexical.
4. The Scope Chain
When the JavaScript engine compiles and executes your code, it creates a Lexical Environment for each running context. This environment contains the local variables and a reference to its outer lexical environment.
When a variable is resolved (read/written), the engine performs a lookup:
- It searches the current local scope.
- If not found, it travels up the outer lexical environment reference.
- It repeats this traversal until it finds the variable or reaches the Global Scope.
- If it reaches the global scope and the variable is still not found, it throws a
ReferenceError(or creates a global variable in non-strict mode if writing).
This nested sequence of environments is called the Scope Chain.
[ Global Scope (outermost) ]
↑
[ Outer Function Scope ]
↑
[ Inner Block/Function Scope (innermost) ]5. Variable Shadowing
Variable shadowing occurs when a variable declared within a local scope has the same name as a variable in an outer scope. The local declaration "shadows" (hides) the outer variable inside that local environment, preventing the scope lookup chain from reaching it.
const value = "global";
function shadowExample() {
const value = "local"; // Shadowing occurs here
console.log(value); // "local"
}
shadowExample();
console.log(value); // "global" (outer variable remains untouched)Shadowing Pitfalls
Shadowing can make code hard to read and debug. Developers may accidentally shadow variables (like parameters or closure-bound values), causing unexpected behavior:
let theme = "dark";
function configureTheme(theme) {
// The parameter 'theme' shadows the outer variable 'theme'
theme = "light"; // Mutates parameter local variable, NOT global theme!
}
configureTheme("light");
console.log(theme); // "dark" (global remains unchanged!)6. Common Interview Coding Traps
Trap #1: Hoisting and Scoping Loop
What is printed by the following code?
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 100);
}Answer:
It prints 3, 3, 3.
Because var is function-scoped (not block-scoped), a single i variable is shared across all loop iterations. When the setTimeout callbacks execute (after the loop finishes), i has already been incremented to 3.
To fix this, change var to let (which creates a new block-scoped binding for each iteration) or use an IIFE closure to isolate the index.
Trap #2: Global Scope Pollution
What happens in the following code?
function calculateTotal() {
// Missing let, const, or var
price = 100;
tax = 10;
return price + tax;
}
calculateTotal();
console.log(price); Answer:
In non-strict mode, price is printed as 100.
Because price was declared without var, let, or const, the engine searches the scope chain. Finding no declaration, it attaches price as a property to the global object (window in browser / global in Node).
In strict mode ('use strict'), this throws a ReferenceError: price is not defined, preventing global pollution.
Trap #3: Parameter Shadowing
What is the output of the following code?
var name = "Alice";
function greet(name) {
console.log(name);
}
greet();Answer:
It prints undefined.
The parameter name shadows the global variable name. Since greet() is called without arguments, the local parameter name defaults to undefined. The scope chain resolves name locally, bypassing the global value.
7. Key Takeaways
- Scope defines variable visibility rules.
- Var is function-scoped; Let/Const are block-scoped (restricting access to curly braces
{}). - JavaScript is Lexically Scoped—scoping is determined by where functions are written in the source code, not where they are executed.
- The Scope Chain is the path resolved by the engine looking up nested parent lexical environments.
- Shadowing occurs when a local variable hides an outer variable of the same name.
- Always use
'use strict'or ESM to prevent accidental global scope leakages.