Scope determines where variables are accessible. Master global, function, block scope and the scope chain for JavaScript interviews.
Picture a set of nested rooms in a house. Each room has its own cabinet of variables. When you need something, you check your own cabinet first. Not there? Walk to the next outer room and check. Keep walking outward until you find it or exit the house entirely and get an error. That chain of rooms is scope. The innermost room is the current function. Each outer room is an enclosing function. The house itself is the global scope. The critical rule: you can always look outward, never inward. A parent room cannot see inside a child room. But a child can see everything in every room it's nested inside. Scope is determined at write time — where a function is written in the source code, not where it's called from. This is called lexical scoping.
Variables declared outside any function or block. In browsers, they become properties of window. Visible everywhere in your program.
var appName = 'JSPrep'
function anywhere() {
console.log(appName) // 'JSPrep' ✓ — visible from anywhere
}
Variables declared inside a function exist only within that function, created when it runs, destroyed when it returns.
function makeGreeting() {
var message = 'Hello'
console.log(message) // 'Hello' ✓
}
makeGreeting()
console.log(message) // ReferenceError — doesn't exist here
let and const are block-scoped — they exist only within the { } block they're declared in. var ignores block boundaries entirely.
if (true) {
let blockVar = 'block-scoped'
var funcVar = 'function-scoped'
}
console.log(funcVar) // 'function-scoped' — var leaked out
console.log(blockVar) // ReferenceError — let stayed inside
When JavaScript looks up a variable, it searches the current scope first, then moves outward until it finds it or reaches global scope. If still not found: ReferenceError.
const global = 'global'
function outer() {
const outerVar = 'outer'
function inner() {
const innerVar = 'inner'
console.log(innerVar) // found in inner ✓
console.log(outerVar) // not in inner → found in outer ✓
console.log(global) // not in outer → found in global ✓
console.log(missing) // not anywhere → ReferenceError ✗
}
console.log(innerVar) // ReferenceError — can't look inward
}
Scope is fixed at the point where the function is written. Calling it from a different location does not change what it can see.
const x = 'global x'
function readX() {
console.log(x) // always reads from where readX was DEFINED
}
function other() {
const x = 'local x'
readX() // still prints 'global x', not 'local x'
}
other() // 'global x'
When an inner scope declares a variable with the same name as an outer one, the inner version shadows the outer. The outer still exists — it's just unreachable from within.
let score = 100
function game() {
let score = 50 // shadows outer score — different variable
console.log(score) // 50
}
game()
console.log(score) // 100 — outer untouched
Classic bug: you intend to modify the outer variable but accidentally declare a new inner one instead. Use ESLint's no-shadow rule to catch this.
Every ES module file has its own scope. Top-level variables are not global — only explicitly exported values are accessible to other modules.
// utils.js
const secret = 'internal' // module-scoped only
export const PI = 3.14159 // accessible to importers
// main.js
import { PI } from './utils.js'
console.log(PI) // 3.14159 ✓
console.log(secret) // ReferenceError
Before ES modules, the Immediately Invoked Function Expression created private scope to avoid polluting globals. You'll see this pattern in legacy code.
(function() {
var private = 'only exists in here'
// all internal logic here
})()
console.log(private) // ReferenceError — perfectly isolated
// Bug 1: Forgetting var/let/const creates a global
function setName() {
name = 'Alice' // no declaration — accidentally global in non-strict mode
}
// Bug 2: var leaks out of for loops
for (var i = 0; i < 3; i++) {}
console.log(i) // 3 — leaked. Use let instead.
// Bug 3: let in a block, used outside it
if (condition) {
let result = compute()
}
return result // ReferenceError — result died with the block
Many devs think scope is determined by where a function is called — but actually JavaScript uses lexical scoping, where scope is determined by where a function is written. Calling from a different location doesn't change what variables it sees.
Many devs think var is block-scoped like let and const — but actually var ignores block boundaries entirely. A var declared inside an if or for block leaks into the entire surrounding function. This is one of the core reasons let was introduced in ES6.
Many devs think inner scopes can't affect outer scopes — but actually a closure can both read and write to outer scope variables. If an inner function assigns to an outer variable (not shadows — assigns), that mutation persists after the inner function returns.
Many devs think top-level variables in a module file are global — but actually ES module files have their own scope. Only exported values cross the boundary. This is fundamentally different from script files without type="module", where top-level vars do become globals.
Many devs think shadowed variables are the same variable — but actually they are completely independent. Changing the inner shadowed variable has zero effect on the outer one. They share a name, not storage.
Many devs think strict mode changes scoping rules — but actually it doesn't change scope. What it does is throw a ReferenceError for undeclared variable assignments instead of silently creating a global. It makes scope bugs visible rather than hiding them.
React's useState returns state scoped to each component instance — because each render call is a function invocation with its own scope, every component gets independent state without any global state management needed for basic use cases.
The "callback hell" pattern in legacy Node.js code was a scope and readability problem — deeply nested callbacks created deeply nested scopes, causing variable name collisions and making the scope chain hard to reason about, which is what Promises and async/await were designed to flatten.
Webpack's scope hoisting optimization analyzes the scope chain across your entire module graph at build time — it merges module scopes where safe to do so, reducing the number of function wrappers and improving runtime performance by up to 10%.
ESLint's no-shadow rule is enabled in virtually every serious production codebase — it was introduced because subtle shadowing bugs have shipped to production at major companies when a developer unknowingly redeclared a variable name that existed in an outer scope.
CSS Modules and CSS-in-JS exist because CSS has no scope — every class name is global, causing unpredictable style collisions at scale. The entire ecosystem of scoped styling tools exists to bring JavaScript-style scoping to stylesheets.
The var-in-for-loop bug affects setTimeout callbacks throughout legacy codebases — because var is function-scoped, the loop variable is shared across all iterations, so async callbacks capture the final value rather than the per-iteration value. Converting to let fixes it because let creates a new binding per iteration.
No questions tagged to this topic yet.
Tag questions in Admin → Questions by setting the "Topic Page" field to javascript-scope-interview-questions.
Reading answers is not the same as knowing them. Practice saying them out loud with AI feedback — that's what builds real interview confidence.