JavaScript BasicsFeatured

JavaScript Hoisting: Understanding Variable and Function Lifting

Master JavaScript hoisting behavior. Learn how variables, functions, and classes are hoisted, common pitfalls, and best practices for avoiding hoisting-related bugs.

By JavaScript Document Team
hoistingvariablesfunctionsbasicsfundamentals

Hoisting is JavaScript's default behavior of moving declarations to the top of their containing scope during the compilation phase. Understanding hoisting is crucial for avoiding bugs and writing predictable JavaScript code.

What is Hoisting?

Hoisting is a JavaScript mechanism where variables and function declarations are moved to the top of their scope before code execution. This means you can use functions and variables before they're declared in your code.

Basic Example of Hoisting

// This works due to hoisting
console.log(greet()); // "Hello, World!"

function greet() {
  return 'Hello, World!';
}

// But this doesn't work as expected
console.log(myVar); // undefined (not ReferenceError)
var myVar = 5;
console.log(myVar); // 5

// What JavaScript actually sees:
// var myVar; // Declaration is hoisted
// console.log(myVar); // undefined
// myVar = 5; // Assignment stays in place
// console.log(myVar); // 5

Function Hoisting

Function declarations are fully hoisted, meaning both the declaration and the function body are moved to the top.

Function Declarations

// You can call the function before it's declared
sayHello(); // "Hello!"

function sayHello() {
  console.log('Hello!');
}

// Multiple function calls before declaration
console.log(add(5, 3)); // 8
console.log(multiply(4, 2)); // 8

function add(a, b) {
  return a + b;
}

function multiply(a, b) {
  return a * b;
}

// Nested function hoisting
function outer() {
  inner(); // Works!

  function inner() {
    console.log('Inner function');
  }
}

outer();

Function Expressions

Function expressions are not hoisted the same way as function declarations.

// This throws an error
// sayHi(); // TypeError: sayHi is not a function

var sayHi = function () {
  console.log('Hi!');
};

sayHi(); // "Hi!" - Works after assignment

// With const or let
// greet(); // ReferenceError: Cannot access 'greet' before initialization

const greet = function () {
  console.log('Greetings!');
};

// Arrow functions behave like function expressions
// welcome(); // ReferenceError

const welcome = () => {
  console.log('Welcome!');
};

Named Function Expressions

// The variable is hoisted, but not the function
console.log(typeof myFunc); // undefined

var myFunc = function namedFunc() {
  console.log('Named function expression');
  // 'namedFunc' is only available inside the function
  console.log(typeof namedFunc); // "function"
};

myFunc(); // "Named function expression"
// namedFunc(); // ReferenceError: namedFunc is not defined

Variable Hoisting

Variable hoisting behaves differently for var, let, and const.

var Hoisting

Variables declared with var are hoisted and initialized with undefined.

console.log(x); // undefined (not ReferenceError)
var x = 5;
console.log(x); // 5

// Multiple var declarations
console.log(a, b, c); // undefined undefined undefined
var a = 1;
var b = 2;
var c = 3;
console.log(a, b, c); // 1 2 3

// In functions
function example() {
  console.log(localVar); // undefined
  var localVar = "I'm local";
  console.log(localVar); // "I'm local"
}

example();

// var in loops
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 3 3 3
}
// 'i' is hoisted to function scope

let and const Hoisting (Temporal Dead Zone)

let and const are hoisted but not initialized, creating a "Temporal Dead Zone".

// Temporal Dead Zone example
// console.log(myLet); // ReferenceError: Cannot access 'myLet' before initialization
let myLet = 5;
console.log(myLet); // 5

// console.log(myConst); // ReferenceError: Cannot access 'myConst' before initialization
const myConst = 10;
console.log(myConst); // 10

// The Temporal Dead Zone in action
let x = 'outer';

function example() {
  // TDZ starts
  // console.log(x); // ReferenceError
  // TDZ ends
  let x = 'inner';
  console.log(x); // "inner"
}

example();

// Block scope with let/const
{
  // console.log(blockVar); // ReferenceError
  let blockVar = "I'm in a block";
  console.log(blockVar); // "I'm in a block"
}
// console.log(blockVar); // ReferenceError: blockVar is not defined

Temporal Dead Zone Explained

// TDZ visualization
function demonstrateTDZ() {
  // TDZ for 'a' starts here
  console.log('Function starts');

  // Accessing 'a' here would throw ReferenceError
  // console.log(a);

  console.log('Before declaration');

  let a = 5; // TDZ for 'a' ends here
  console.log(a); // 5 - Safe to access
}

// TDZ with const
function constTDZ() {
  // const b; // SyntaxError: Missing initializer
  const b = 10; // Must be initialized
  console.log(b);
}

// TDZ in switch statements
function switchTDZ(value) {
  switch (value) {
    case 1:
      // console.log(x); // ReferenceError if case 1 executes
      let x = 'case 1';
      console.log(x);
      break;
    case 2:
      // x is not accessible here
      let y = 'case 2';
      console.log(y);
      break;
  }
}

Class Hoisting

Classes are hoisted but remain in the Temporal Dead Zone until declaration.

// const myInstance = new MyClass(); // ReferenceError

class MyClass {
  constructor() {
    this.name = 'MyClass';
  }
}

const myInstance = new MyClass(); // Works after declaration
console.log(myInstance.name); // "MyClass"

// Class expressions
// const instance = new MyClassExpr(); // ReferenceError

const MyClassExpr = class {
  constructor() {
    this.type = 'expression';
  }
};

// Function vs Class hoisting difference
greet(); // Works - function hoisting

function greet() {
  console.log('Hello from function');
}

// new Greeting(); // ReferenceError - class not hoisted

class Greeting {
  sayHello() {
    console.log('Hello from class');
  }
}

Hoisting in Different Scopes

Global Scope Hoisting

// Global scope
console.log(globalVar); // undefined
var globalVar = "I'm global";

// Function declarations in global scope
console.log(typeof globalFunc); // "function"
function globalFunc() {
  return 'Global function';
}

// Window object (in browsers)
console.log(window.globalVar); // undefined initially
console.log(window.globalFunc); // function globalFunc()

Function Scope Hoisting

function outerFunction() {
  // All declarations are hoisted to the top of the function
  console.log(innerVar); // undefined
  console.log(innerFunc()); // "Inner function result"

  var innerVar = 'Inner variable';

  function innerFunc() {
    return 'Inner function result';
  }

  // Nested function scope
  function nestedExample() {
    console.log(deepVar); // undefined
    var deepVar = 'Deeply nested';

    // 'innerVar' is accessible from parent scope
    console.log(innerVar); // "Inner variable"
  }

  nestedExample();
}

outerFunction();

Block Scope Hoisting

// Block scope with let and const
{
  // console.log(blockLet); // ReferenceError
  let blockLet = 'Block scoped with let';
  const blockConst = 'Block scoped with const';

  // var ignores block scope
  var blockVar = 'Not block scoped';
}

console.log(blockVar); // "Not block scoped"
// console.log(blockLet); // ReferenceError
// console.log(blockConst); // ReferenceError

// In if statements
if (true) {
  // console.log(ifLet); // ReferenceError
  let ifLet = 'Inside if';
  var ifVar = 'Also inside if';
}

console.log(ifVar); // "Also inside if"
// console.log(ifLet); // ReferenceError

Common Hoisting Pitfalls

The Classic Loop Problem

// Problem with var in loops
for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i); // Prints 5, 5, 5, 5, 5
  }, 100);
}

// Solution 1: Use let
for (let j = 0; j < 5; j++) {
  setTimeout(function () {
    console.log(j); // Prints 0, 1, 2, 3, 4
  }, 100);
}

// Solution 2: IIFE (before let/const)
for (var k = 0; k < 5; k++) {
  (function (index) {
    setTimeout(function () {
      console.log(index); // Prints 0, 1, 2, 3, 4
    }, 100);
  })(k);
}

// Solution 3: Function factory
function createLogger(index) {
  return function () {
    console.log(index);
  };
}

for (var m = 0; m < 5; m++) {
  setTimeout(createLogger(m), 100);
}

Variable Shadowing

var x = 1;

function shadowExample() {
  console.log(x); // undefined (not 1!)
  var x = 2; // This shadows the outer x
  console.log(x); // 2
}

shadowExample();
console.log(x); // 1

// With let - throws error
let y = 1;

function letShadow() {
  // console.log(y); // ReferenceError
  let y = 2;
  console.log(y); // 2
}

letShadow();

Function Name Conflicts

// Function overwriting
function foo() {
  return 'First foo';
}

function foo() {
  return 'Second foo'; // This overwrites the first
}

console.log(foo()); // "Second foo"

// Variable and function name conflicts
function bar() {
  return "I'm a function";
}

var bar = "I'm a variable"; // Overwrites the function
console.log(bar); // "I'm a variable"
// bar(); // TypeError: bar is not a function

Best Practices

1. Declare Variables at the Top

// Good practice
function goodExample() {
  let a, b, c;
  const MAX_SIZE = 100;

  // Use variables after declaration
  a = 5;
  b = 10;
  c = a + b;

  return c;
}

// Avoid scattered declarations
function avoidThis() {
  console.log(someLogic());

  var x = 5; // Declared in the middle

  function someLogic() {
    return x; // undefined when called above
  }
}

2. Use const and let Instead of var

// Modern approach
const CONFIG = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
};

let userCount = 0;

function incrementUser() {
  userCount++;
}

// Avoid var
// var config = {}; // Can be reassigned and hoisted
// var count = 0;   // Function-scoped, not block-scoped

3. Initialize Variables When Declaring

// Good
const userName = 'John';
let isLoggedIn = false;
let currentScore = 0;

// Avoid
let userName; // undefined
let isLoggedIn;
userName = 'John';
isLoggedIn = false;

4. Place Functions Before Calls (Even Though Hoisted)

// Readable and clear
function calculateTotal(items) {
  return items.reduce((sum, item) => sum + item.price, 0);
}

function formatCurrency(amount) {
  return `$${amount.toFixed(2)}`;
}

// Use functions after declaration
const items = [{ price: 10 }, { price: 20 }];
const total = calculateTotal(items);
console.log(formatCurrency(total));

Advanced Hoisting Concepts

Hoisting in Strict Mode

'use strict';

// Still works the same for functions
console.log(strictFunc()); // "Strict function"

function strictFunc() {
  return 'Strict function';
}

// But prevents some bad practices
// x = 5; // ReferenceError: x is not defined
// Must declare variables
let x = 5;

// No hoisting for undeclared variables
function nonStrictExample() {
  // Without strict mode, creates global variable
  // undeclaredVar = 10;
}

Module Scope Hoisting

// In ES6 modules
// import statements are hoisted
console.log(myImport); // Works if myImport is exported

import { myImport } from './module.js';

// But the module must be evaluated first
// Circular dependencies can cause issues

// Export hoisting
export function exportedFunc() {
  return "I'm exported";
}

// Can be used before in the same module
console.log(exportedFunc());

Dynamic Import and Hoisting

// Dynamic imports are not hoisted
async function loadModule() {
  // This is not hoisted
  const module = await import('./dynamic-module.js');
  console.log(module.default);
}

// Regular imports are hoisted
import staticModule from './static-module.js';
console.log(staticModule); // Available immediately

Debugging Hoisting Issues

// Use typeof to check if variable is declared
if (typeof myVariable !== 'undefined') {
  console.log(myVariable);
}

// Check for temporal dead zone
try {
  console.log(myLetVar);
} catch (e) {
  console.log('Variable is in TDZ:', e.message);
}
let myLetVar = 5;

// Use debugger to inspect hoisting
function debugHoisting() {
  debugger; // Pause here to inspect scope
  console.log(a); // undefined
  var a = 5;
  console.log(a); // 5
}

// Console logging for understanding execution order
console.log('1. Start of script');

console.log('2. typeof myFunc:', typeof myFunc);
console.log('3. typeof myVar:', typeof myVar);

function myFunc() {
  console.log('4. Inside myFunc');
}

var myVar = 'value';

console.log('5. End of script');

Conclusion

Hoisting is a fundamental JavaScript behavior that can lead to confusion if not properly understood. While function declarations are fully hoisted and can be used before their declaration, variables declared with var are hoisted but initialized with undefined, and let/const declarations remain in a temporal dead zone until their declaration is reached. Understanding these behaviors helps write more predictable code and avoid common pitfalls. Following best practices like using const and let, declaring variables at the top of their scope, and being explicit about initialization will lead to cleaner, more maintainable JavaScript code.