FrontendPrep
Menu
Topics
Questions
Guides
Challenges
Soon
Back to JavaScript Questions
javascriptMedium

JavaScript Scopes: Global, Function, and Lexical Scope

Master JavaScript scoping rules for technical interviews. Learn Global Scope, Function Scope, Block Scope, Lexical Scope vs. Dynamic Scope, Scope Chains, and variable shadowing.

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 count without 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 var globally get attached to the window object (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 defined

C. 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 defined

D. 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:

global

Why 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 this keyword 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:

  1. It searches the current local scope.
  2. If not found, it travels up the outer lexical environment reference.
  3. It repeats this traversal until it finds the variable or reaches the Global Scope.
  4. 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.

Share this Resource

Help other developers level up by sharing this study guide.

More Technical Questions

Expand your mastery. Deep dive into other frontend interview challenges in this category.