JavaScript: Variable Scoping (In-Depth)

In JavaScript, scoping determines where variables and functions are accessible. Understanding how variables are scoped is essential to writing reliable, bug-free code. This guide covers all aspects of variable scoping in detail.

What is Scope?

Scope is the region in your code where a variable is defined and can be referenced. Outside this region, the variable is invisible or inaccessible.

Scope Type Keyword(s) Description
Global Scope var, let, const Declared outside all functions/blocks. Accessible anywhere in the file.
Function Scope var Declared inside a function with var. Accessible anywhere in that function.
Block Scope let, const Declared inside a block ({ ... }), such as if/for/while. Only accessible within that block.

Global Scope

A variable declared outside any function or block becomes global. Global variables are attached to the window object in browsers.

var ocean = "Pacific";

function printOcean() {
  console.log(ocean); // Accessible: prints "Pacific"
}
printOcean();
    

Function Scope (var)

var is function-scoped, meaning it's visible throughout the entire function in which it is declared, even before its declaration due to hoisting:

function demoVar() {
  if (true) {
    var fish = "tuna";
  }
  console.log(fish); // "tuna" (still accessible outside the if block)
}
demoVar();
    
// Output:
tuna

Block Scope (let and const)

let and const are block-scoped. They are only available within the block { ... } where they are defined:

function demoLetConst() {
  if (true) {
    let crab = "blue";
    const shell = "hard";
    console.log(crab); // blue
  }
  // console.log(crab); // ReferenceError: crab is not defined
}
demoLetConst();
    
// Output:
blue
Block-scoped variables help prevent accidental leaks and bugs.

Hoisting

Hoisting is JavaScript's behavior of moving variable and function declarations to the top of their scope before executing code. Only var declarations are hoisted and initialized with undefined. let and const are hoisted but not initialized, leading to a "Temporal Dead Zone".

console.log(a); // undefined (hoisted)
var a = 5;

// console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 10;
    
// Output:
undefined
Best Practice: Always declare variables at the top of their scope and use let or const for block scoping.

Shadowing

A variable declared within a certain scope "shadows" (overrides) a variable with the same name in an outer scope.

let animal = "dolphin";
function zoo() {
  let animal = "shark";
  console.log(animal); // shark
}
zoo();
console.log(animal); // dolphin
    
// Output:
shark
dolphin

Lexical Scope

JavaScript uses lexical scoping: a function’s scope is determined by where it’s defined, not where it’s called.

let fish = "goldfish";
function outer() {
  let fish = "salmon";
  function inner() {
    console.log(fish);
  }
  inner();
}
outer(); // "salmon"
    

Summary Table

KeywordScopeHoistedRe-declarableRe-assignable
varFunctionYes (initialized with undefined)YesYes
letBlockYes (not initialized)NoYes
constBlockYes (not initialized)NoNo

Why Does Scoping Matter?

Pro Tip: Use const by default, let if the variable changes, and avoid var in modern code.