scope in JavaScript: types and working principles
Scope in JavaScript defines where a variable can be read or written. The engine resolves scope at compile time, not at runtime. That is why it's called lexical scope.
Theory
TL;DR
- Scope = the region of code where a variable exists and can be accessed
- Three types: global (entire codebase), function (inside one function), block (inside one
{}) varrespects function scope only;letandconstrespect block scope- Inner scopes can read outer variables, but not the reverse
- Use
let/constin the smallest block possible; avoidvar
Quick example
const globalVar = 'global'; // global scope
function outer() {
const outerVar = 'outer'; // function scope
if (true) {
let blockVar = 'block'; // block scope
console.log(globalVar); // "global" - reads up the chain
console.log(outerVar); // "outer" - reads up the chain
console.log(blockVar); // "block" - local
}
console.log(blockVar); // ReferenceError - block is gone
}
outer();
console.log(outerVar); // ReferenceError - function scope is goneEach scope can read from its parent, but the parent cannot reach into the child.
Scope chain
JavaScript uses lexical scoping: scope is determined by where code is written, not where it runs. When the engine looks up a variable, it starts in the current scope and walks up the chain toward global until it finds the variable or throws a ReferenceError.
V8 creates a LexicalEnvironment object for each scope during compilation. These objects link to their parent via an outer reference, forming the chain. Variable lookups follow those links one step at a time.
When to use
- Global scope: app-wide constants like
process.env.NODE_ENVor a shared config object - Function scope: isolate logic inside a utility or handler so it doesn't affect other functions
- Block scope: limit a variable to a
forloop,ifblock, ortry/catchwithout letting it leak
The narrower the scope, the fewer surprises in code review.
var, let, and const
var binds to the nearest function scope (or global if outside all functions). It ignores blocks entirely. let and const bind to the nearest block. They're also subject to the Temporal Dead Zone (TDZ): the variable exists in scope from the start of the block, but any access before the declaration line throws a ReferenceError.
console.log(typeof a); // "undefined" - var hoisted, initialized to undefined
console.log(typeof b); // ReferenceError - let is in TDZ
var a = 1;
let b = 2;This trips up developers with years of experience. I've seen it cause confusion in code reviews when someone assumes typeof is always safe before a declaration.
Common mistakes
var in a loop
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 3 3 3
}All three callbacks close over the same i because var is function-scoped. By the time they run, the loop is done and i is 3. Fix: use let, which creates a new binding per iteration.
Assuming var respects blocks
if (true) {
var x = 5;
}
console.log(x); // 5 - leaks out of the blockvar only stops at function boundaries. Use let or const here.
Shadowing without noticing
const user = 'Alice';
function greet() {
const user = 'Bob'; // shadows outer user
console.log(user); // "Bob"
}
greet();
console.log(user); // "Alice"Shadowing is valid JavaScript, but it makes the code harder to trace. Rename the inner variable when possible.
Real-world usage
- React:
useEffectcloses over component scope variables. Ifuserschanges, a new effect captures the fresh reference - Express: route handlers close over the
appobject from module scope - Node.js:
requirewraps each module in a function, giving it its own scope forexports - Redux thunks: capture
dispatchfrom the connected component's scope
Follow-up questions
Q: What does console.log(x); var x = 1; print?
A: undefined. The declaration is hoisted to the top of the function scope, but the initialization stays in place.
Q: Why does for (var i...) with setTimeout print the same number three times?
A: var creates one binding shared across all iterations. All closures point to the same i. Use let to get a separate binding per iteration.
Q: What is the Temporal Dead Zone?
A: TDZ is the period between when a let/const variable enters scope (start of the block) and when the declaration line is reached. Any access in that window throws a ReferenceError.
Q: Does top-level let create a global variable?
A: No. var x = 1 at the top level adds x to window in a browser. let x = 1 creates a block-scoped variable that is not a property of window.
Q: How does V8 optimize scope lookups in hot loops?
A: V8 uses inline caching. After the first lookup, the engine records the type and location of the variable. Subsequent accesses in the same scope skip the chain walk entirely. Keeping variables in tight scopes helps this optimization.
Examples
Basic: scope chain in action
const language = 'JavaScript';
function describe() {
const topic = 'scope';
function announce() {
const detail = 'lexical';
console.log(`${detail} ${topic} in ${language}`);
// "lexical scope in JavaScript"
// Each variable comes from a different scope level
}
announce();
}
describe();announce reads detail from its own scope, topic from its parent function, and language from global. The lookup always goes from inside out, never the other way.
Intermediate: var vs let in a loop
This comes up in JavaScript interviews constantly. Two versions, two different outputs:
// var: all closures share one binding
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log('var:', i), 0);
}
// Output: var: 3 var: 3 var: 3
// let: each iteration gets its own binding
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log('let:', j), 0);
}
// Output: let: 0 let: 1 let: 2The difference is entirely scope rules. var is function-scoped, so there's one i for the whole loop. let is block-scoped, so each loop body gets a fresh j that belongs to that iteration only.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.