Labs ICT
Pro Login

Hoisting

Hoisting is JavaScript's behavior of moving declarations to the top of their containing scope during compilation. This means you can use variables and functions before they appear in your code, but understanding how hoisting works differently for variables, functions, and classes is crucial for writing predictable JavaScript code.

In this tutorial, we'll explore hoisting mechanisms, how it affects different declaration types, the impact of strict mode, and best practices for avoiding hoisting-related bugs.

Understanding Hoisting

Hoisting is JavaScript's default behavior of moving declarations to the top.

What is Hoisting?

// Hoisting example
console.log(myVar); // undefined (not ReferenceError)
var myVar = 5;
console.log(myVar); // 5

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

// Function hoisting
console.log(myFunction()); // "Hello"
function myFunction() {
  return "Hello";
}

// Function expressions are not hoisted the same way
console.log(myExpression); // undefined
var myExpression = function() {
  return "World";
};
console.log(myExpression()); // "World"

Compilation vs Execution

// JavaScript compilation phase
// 1. Find all declarations
// 2. Allocate memory for them
// 3. Move them to top of scope

// Example showing the two phases
function example() {
  console.log(a); // undefined (hoisted)
  console.log(b); // ReferenceError (not hoisted)
  
  var a = 1;
  let b = 2;
  
  console.log(a); // 1
  console.log(b); // 2
}

// What JavaScript sees during compilation:
function example() {
  var a; // Hoisted declaration
  // let b; // Not hoisted (temporal dead zone)
  
  console.log(a); // undefined
  console.log(b); // ReferenceError
  
  a = 1; // Assignment
  let b = 2; // Declaration and assignment
  
  console.log(a); // 1
  console.log(b); // 2
}

Variable Hoisting

Different variable declarations behave differently with hoisting.

var Hoisting

// var declarations are hoisted
function testVar() {
  console.log(myVar); // undefined
  var myVar = 'declared';
  console.log(myVar); // 'declared'
}

testVar();

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

// var hoisting with same name
function sameName() {
  var x = 1;
  
  if (true) {
    var x = 2; // Same variable, not block-scoped
  }
  
  console.log(x); // 2
}

// Function-scoped var
function varScope() {
  var x = 'outer';
  
  if (true) {
    var x = 'inner'; // Re-declares same variable
    console.log(x); // 'inner'
  }
  
  console.log(x); // 'inner'
}

let and const Hoisting

// let and const are not hoisted in the same way
function testLet() {
  console.log(myLet); // ReferenceError
  let myLet = 'declared';
  console.log(myLet); // 'declared'
}

// Temporal Dead Zone (TDZ)
function temporalDeadZone() {
  // TDZ starts here for myVar
  // console.log(myVar); // ReferenceError
  
  let myVar = 'value';
  // TDZ ends here
  
  console.log(myVar); // 'value'
}

// const hoisting
function testConst() {
  // console.log(myConst); // ReferenceError
  const myConst = 'immutable';
  console.log(myConst); // 'immutable'
}

// Block scoping with let/const
function blockScope() {
  let x = 'outer';
  
  if (true) {
    let x = 'inner'; // Different variable, block-scoped
    console.log(x); // 'inner'
  }
  
  console.log(x); // 'outer'
}

// const with objects
const obj = { name: 'John' };
obj.name = 'Jane'; // Allowed (object is mutable)
// obj = {}; // Error (cannot reassign const)

Function Hoisting

Functions are hoisted differently depending on how they're declared.

Function Declarations

// Function declarations are fully hoisted
console.log(myFunction()); // 'Hello'
function myFunction() {
  return 'Hello';
}

// Function declarations override variable declarations
function overrideExample() {
  console.log(typeof myVar); // 'function'
  
  function myVar() {
    return 'I am a function';
  }
  
  console.log(myVar()); // 'I am a function'
}

// Hoisting with same name functions
function sameName() {
  console.log(first()); // 'First'
  
  function first() {
    return 'First';
  }
  
  function second() {
    return 'Second';
  }
  
  console.log(second()); // 'Second'
}

// Last function wins
function lastWins() {
  console.log(myFunc()); // 'Second'
  
  function myFunc() {
    return 'First';
  }
  
  function myFunc() {
    return 'Second';
  }
}

Function Expressions

// Function expressions are not hoisted
console.log(myExpression); // undefined
// console.log(myExpression()); // TypeError

var myExpression = function() {
  return 'Expression';
};

console.log(myExpression()); // 'Expression'

// Named function expressions
console.log(namedExpression); // undefined

var namedExpression = function myName() {
  return 'Named';
};

console.log(namedExpression()); // 'Named'
// console.log(myName()); // ReferenceError (not in outer scope)

// Arrow functions
console.log(arrowFunc); // undefined

var arrowFunc = () => 'Arrow';
console.log(arrowFunc()); // 'Arrow'

Class Hoisting

Classes have different hoisting behavior than functions.

Class Declaration Hoisting

// Class declarations are not hoisted
// console.log(MyClass); // ReferenceError
// const instance = new MyClass(); // ReferenceError

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

console.log(MyClass); // [Function: MyClass]
const instance = new MyClass();
console.log(instance.name); // 'Class'

// Class expressions
console.log(MyExpression); // undefined

const MyExpression = class {
  constructor() {
    this.type = 'Expression';
  }
};

console.log(MyExpression); // [Function: MyExpression]

Class Temporal Dead Zone

// Classes have TDZ like let/const
function classTDZ() {
  // console.log(MyClass); // ReferenceError
  
  class MyClass {
    constructor() {
      this.value = 'test';
    }
  }
  
  console.log(MyClass); // [Function: MyClass]
  const instance = new MyClass();
  console.log(instance.value); // 'test'
}

// Extending hoisted classes
function extendExample() {
  // console.log(ChildClass); // ReferenceError
  
  class ParentClass {
    constructor() {
      this.parent = true;
    }
  }
  
  class ChildClass extends ParentClass {
    constructor() {
      super();
      this.child = true;
    }
  }
  
  console.log(ChildClass); // [Function: ChildClass]
  const child = new ChildClass();
  console.log(child.parent, child.child); // true, true
}

Hoisting in Different Scopes

Hoisting behavior varies across different scope types.

Global Scope Hoisting

// Global scope hoisting
console.log(globalVar); // undefined
console.log(globalFunction()); // 'Global Function'

var globalVar = 'global';
function globalFunction() {
  return 'Global Function';
}

console.log(globalVar); // 'global'
console.log(globalFunction()); // 'Global Function'

// In browsers, global variables become properties of window
console.log(window.globalVar); // 'global'
console.log(window.globalFunction); // [Function: globalFunction]

// let/const in global scope
console.log(globalLet); // ReferenceError
let globalLet = 'global let';
console.log(globalLet); // 'global let'

Function Scope Hoisting

// Function scope with var
function functionScope() {
  console.log(innerVar); // undefined
  
  if (false) {
    var innerVar = 'This still gets hoisted';
  }
  
  console.log(innerVar); // undefined
}

// Block scope with let/const
function blockScope() {
  // console.log(blockVar); // ReferenceError
  
  {
    console.log(blockVar); // ReferenceError (TDZ)
    let blockVar = 'Block scoped';
    console.log(blockVar); // 'Block scoped'
  }
  
  console.log(blockVar); // ReferenceError
}

// Mixed hoisting
function mixedHoisting() {
  console.log(a, b, c); // undefined, ReferenceError, ReferenceError
  
  var a = 'var';
  let b = 'let';
  const c = 'const';
  
  console.log(a, b, c); // 'var', 'let', 'const'
}

Strict Mode and Hoisting

Strict mode changes how hoisting behaves, making it more predictable.

Strict Mode Impact

'use strict';

// In strict mode, this behavior changes
function strictFunction() {
  console.log(this); // undefined (not window)
}

strictFunction();

// Assigning to undeclared variables throws error
function undeclaredAssignment() {
  // undeclaredVar = 'value'; // ReferenceError
  var declaredVar = 'value'; // OK
}

// Deleting variables throws error
function deleteTest() {
  var myVar = 'test';
  // delete myVar; // SyntaxError
}

// Duplicate parameter names throw error
function duplicateParams(a, a, b) { // SyntaxError
  return a + b;
}

// Duplicate property names in object literals
const obj = {
  prop: 1,
  prop: 2 // SyntaxError in strict mode
};

Strict Mode Best Practices

// Always use strict mode at top level
'use strict';

// Or in functions
function safeFunction() {
  'use strict';
  // Function body
}

// Or in modules (automatically strict)
// myModule.js
export function moduleFunction() {
  // Automatically in strict mode
}

// Combining strict mode with proper hoisting
function strictHoisting() {
  'use strict';
  
  // Declare all variables at top
  var var1, var2, var3;
  
  // Initialize later
  var1 = 'value1';
  var2 = 'value2';
  var3 = 'value3';
  
  console.log(var1, var2, var3);
}

Common Hoisting Issues

Understanding common problems caused by hoisting.

Variable Shadowing

// Variable shadowing issues
var name = 'Global';

function showName() {
  console.log(name); // undefined (hoisted local name)
  var name = 'Local';
  console.log(name); // 'Local'
}

showName();
console.log(name); // 'Global'

// Function declaration shadowing
function test() {
  console.log(myFunc); // [Function: myFunc] (hoisted)
  
  function myFunc() {
    return 'Inner';
  }
  
  var myFunc = function() { // Variable declaration
    return 'Outer';
  };
  
  console.log(myFunc); // [Function: Outer] (assignment)
  console.log(myFunc()); // 'Outer'
}

test();

Order Dependencies

// Order dependency issues
function orderIssue() {
  console.log(result); // undefined
  
  // This depends on hoisting order
  result = calculate();
  
  function calculate() {
    return 42;
  }
  
  var result;
  console.log(result); // 42
}

// Better approach
function betterApproach() {
  function calculate() {
    return 42;
  }
  
  var result = calculate();
  console.log(result); // 42
}

// Function declaration order matters
function orderMatters() {
  console.log(first()); // 'Second' (last declaration wins)
  
  function first() {
    return 'First';
  }
  
  function second() {
    return 'Second';
  }
}

Best Practices

Guidelines for avoiding hoisting-related issues.

Declaration Strategies

// Declare variables at the top
function goodFunction() {
  var var1, var2, var3;
  
  var1 = 'value1';
  var2 = 'value2';
  var3 = 'value3';
  
  // Use variables here
}

// Use let/const for block scoping
function modernFunction() {
  const constant = 'immutable';
  let variable = 'mutable';
  
  if (true) {
    const blockConstant = 'block immutable';
    let blockVariable = 'block mutable';
    // Use block-scoped variables
  }
  
  // constant and variable still accessible
  // blockConstant and blockVariable are not
}

// Avoid function declarations in blocks
function avoidBlockFunctions() {
  // Bad:
  if (true) {
    function blockFunction() {
      return 'bad';
    }
  }
  
  // Good:
  const blockFunction = function() {
    return 'good';
  };
  
  if (true) {
    // Use blockFunction here
  }
}

Code Organization

// Use function expressions for clarity
function organizedCode() {
  // Declare all functions first
  var helper1, helper2;
  
  helper1 = function() {
    return 'Helper 1';
  };
  
  helper2 = function() {
    return 'Helper 2';
  };
  
  // Main logic
  console.log(helper1());
  console.log(helper2());
}

// Use modules to avoid global scope issues
// module.js
export function moduleFunction() {
  return 'Module function';
}

export const moduleConstant = 'Module constant';

// main.js
import { moduleFunction, moduleConstant } from './module.js';
console.log(moduleFunction());
console.log(moduleConstant);

// Use classes instead of constructor functions
class Person {
  constructor(name) {
    this.name = name;
  }
  
  greet() {
    return `Hello, ${this.name}`;
  }
}

// No hoisting issues with classes
const john = new Person('John');

Hoisting Detection

Techniques for detecting and understanding hoisting behavior.

Debugging Hoisting

// Function to check hoisting
function checkHoisting() {
  console.log('Before declaration:');
  console.log('testVar:', typeof testVar);
  console.log('testFunc:', typeof testFunc);
  console.log('testLet:', typeof testLet);
  
  try {
    console.log('testLet value:', testLet);
  } catch (e) {
    console.log('testLet error:', e.message);
  }
  
  var testVar = 'var value';
  function testFunc() {
    return 'function value';
  }
  let testLet = 'let value';
  
  console.log('\nAfter declaration:');
  console.log('testVar:', testVar);
  console.log('testFunc():', testFunc());
  console.log('testLet:', testLet);
}

checkHoisting();

// Analyze hoisting behavior
function analyzeHoisting() {
  const declarations = [];
  
  // Check what's available before declarations
  for (const name in this) {
    if (typeof this[name] !== 'undefined') {
      declarations.push({ name, type: typeof this[name], hoisted: true });
    }
  }
  
  var testVar = 'test';
  function testFunc() {}
  let testLet = 'test';
  
  return declarations;
}

Linting and Tools

// ESLint rules for hoisting
/* eslint-env node, es6 */
/* eslint no-var: "error" */
/* eslint prefer-const: "error" */
/* eslint no-use-before-define: "error" */

// This would trigger ESLint errors
function badExample() {
  console.log(testVar); // no-use-before-define error
  var testVar = 'value';
  
  if (true) {
    var testVar2 = 'another'; // no-var error
  }
}

// Good example
function goodExample() {
  const testVar = 'value';
  
  if (true) {
    const testVar2 = 'another';
  }
  
  console.log(testVar);
}

// TypeScript catches hoisting issues
function typeScriptExample() {
  // console.log(myVar); // TypeScript error: cannot find name 'myVar'
  let myVar: string = 'value';
  console.log(myVar);
}

Summary

Key Takeaways

  • Hoisting moves declarations to top of their scope
  • var declarations are hoisted, let/const are not
  • Function declarations are fully hoisted
  • Function expressions are only the variable part that's hoisted
  • Classes are not hoisted and have temporal dead zone

Best Practices

  • Always use strict mode for predictable behavior
  • Prefer let/const over var for block scoping
  • Declare variables at the top of their scope
  • Use function expressions to avoid hoisting confusion
  • Use linters to catch hoisting issues

Common Pitfalls

  • Assuming let/const are hoisted like var
  • Variable shadowing causing unexpected behavior
  • Function declarations in conditional blocks
  • Order dependencies between hoisted items
  • Not understanding temporal dead zone