JavaScript
06 / 07

Closures, Scopes & Hoisting

JavaScript Closures, Scopes & Hoisting

Understanding scope, closures, and hoisting is fundamental to mastering JavaScript. These concepts are frequently tested in interviews.

Closures

A closure is a function that has access to variables in its outer (enclosing) lexical scope, even after the outer function has returned.

// Basic closure
function outer() {
  const message = 'Hello';
  
  function inner() {
    console.log(message);  // Accesses outer scope
  }
  
  return inner;
}

const fn = outer();
fn();  // 'Hello' - even though outer() has returned

// Counter with closure
function createCounter() {
  let count = 0;  // Private variable
  
  return {
    increment() {
      return ++count;
    },
    decrement() {
      return --count;
    },
    getCount() {
      return count;
    }
  };
}

const counter = createCounter();
console.log(counter.increment());  // 1
console.log(counter.increment());  // 2
console.log(counter.getCount());   // 2
// console.log(counter.count);     // undefined - private!

// Module pattern with closure
const calculator = (function() {
  let result = 0;  // Private state
  
  return {
    add(n) {
      result += n;
      return this;
    },
    subtract(n) {
      result -= n;
      return this;
    },
    getResult() {
      return result;
    },
    reset() {
      result = 0;
      return this;
    }
  };
})();

calculator.add(5).add(3).subtract(2);  // Chaining
console.log(calculator.getResult());   // 6

// Event handlers with closures
function createClickHandlers() {
  const handlers = [];
  
  for (let i = 0; i < 3; i++) {
    handlers.push(function() {
      console.log(`Button ${i} clicked`);
    });
  }
  
  return handlers;
}

const handlers = createClickHandlers();
handlers[0]();  // 'Button 0 clicked'
handlers[1]();  // 'Button 1 clicked'
handlers[2]();  // 'Button 2 clicked'

Scope Types

// Global scope
var globalVar = 'I am global';

// Function scope
function myFunction() {
  var functionVar = 'I am function-scoped';
  console.log(globalVar);      // ✅ Can access global
  console.log(functionVar);    // ✅ Can access function
}

// console.log(functionVar);   // ❌ Error - not accessible outside

// Block scope (let & const)
if (true) {
  var varVariable = 'var is function scoped';
  let letVariable = 'let is block scoped';
  const constVariable = 'const is block scoped';
}

console.log(varVariable);    // ✅ Accessible
// console.log(letVariable);  // ❌ Error
// console.log(constVariable);// ❌ Error

// Lexical scope
function outer() {
  const outerVar = 'outer';
  
  function middle() {
    const middleVar = 'middle';
    
    function inner() {
      const innerVar = 'inner';
      console.log(innerVar);   // ✅ Own scope
      console.log(middleVar);  // ✅ Parent scope
      console.log(outerVar);   // ✅ Grandparent scope
    }
    
    inner();
  }
  
  middle();
}

Hoisting

// Variable hoisting with var
console.log(x);  // undefined (not error!)
var x = 5;

// Equivalent to:
// var x;
// console.log(x);
// x = 5;

// let and const are NOT hoisted to accessible state
// console.log(y);  // ❌ ReferenceError: Cannot access before initialization
let y = 5;

// Function hoisting
greet();  // ✅ Works! Function declarations are hoisted

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

// Function expressions are NOT hoisted
// sayHi();  // ❌ Error
const sayHi = function() {
  console.log('Hi');
};

// Class hoisting - NOT hoisted
// const p = new Person();  // ❌ Error
class Person {
  constructor(name) {
    this.name = name;
  }
}

this Keyword

// Global context
console.log(this);  // Window (browser) or global (Node.js)

// Object method
const person = {
  name: 'John',
  greet() {
    console.log(this.name);  // 'this' refers to person
  }
};

person.greet();  // 'John'

// Losing 'this' context
const greetFn = person.greet;
greetFn();  // undefined - 'this' is now global

// Solutions:
// 1. Arrow function (lexical 'this')
const person2 = {
  name: 'Jane',
  greet: function() {
    setTimeout(() => {
      console.log(this.name);  // 'this' refers to person2
    }, 100);
  }
};

// 2. bind()
const boundGreet = person.greet.bind(person);
boundGreet();  // 'John'

// 3. call() and apply()
const otherPerson = { name: 'Bob' };
person.greet.call(otherPerson);  // 'Bob'
person.greet.apply(otherPerson); // 'Bob'

// Constructor function
function Car(make, model) {
  this.make = make;
  this.model = model;
}

const car = new Car('Toyota', 'Camry');
console.log(car.make);  // 'Toyota'

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free