Hoisting and Closures

Hoisting and Closures explained in details

ยท

4 min read

Hoisting and Closures

Hoisting in JavaScript:

Hoisting is a JavaScript mechanism where variables and function declarations are moved ("hoisted") to the top of their scope (global or function scope) before the code execution. This means that regardless of where variables and functions are declared in the code, they are treated as if they are at the top of their scope.

1. Hoisting of Variables:

Variables declared with var are hoisted to the top, but their initial values are not hoisted. They are initialized with undefined. This is why accessing a var variable before its declaration does not throw an error, but results in undefined.

Example:

console.log(x);  // Output: undefined
var x = 10;

What happens internally:

var x;          // Declaration hoisted to the top
console.log(x); // Output: undefined
x = 10;         // Assignment happens where it is in the code
  • let and const variables are hoisted too, but they are not initialized with undefined. Instead, they enter a "temporal dead zone" from the start of the block until they are declared. Accessing them before their declaration results in a ReferenceError.

Example:

console.log(a); // ReferenceError
let a = 5;

2. Hoisting of Functions:

Function declarations are fully hoisted, meaning both the declaration and the function body are moved to the top of their scope. This allows you to call a function before you define it in the code.

Example:

myFunction();  // Output: "Hello"
function myFunction() {
    console.log("Hello");
}

What happens internally:

function myFunction() {
    console.log("Hello");
}
myFunction(); // Output: "Hello"

However, function expressions (functions assigned to a variable) are hoisted differently. Only the variable declaration is hoisted, not the function itself. So calling a function expression before the variable assignment will result in undefined or a TypeError.

Example:

console.log(sayHello); // Output: undefined
var sayHello = function() {
    console.log("Hi");
};

This behaves as:

var sayHello;          // Variable declaration hoisted
console.log(sayHello); // Output: undefined
sayHello = function() {
    console.log("Hi");
};

In case of let and const:

console.log(sayHello); // ReferenceError
let sayHello = function() {
    console.log("Hi");
};

Closures in JavaScript:

Closures are a fundamental concept in JavaScript where an inner function has access to variables from its outer (enclosing) function, even after the outer function has finished execution. The closure "remembers" the variables and can use them later.

A closure gives an inner function access to three scopes:

  1. Its own scope (variables defined within it).

  2. The outer functionโ€™s scope (variables defined in the outer function).

  3. The global scope.

1. Basic Example of Closure:

function outerFunction() {
    let outerVariable = "I'm from the outer scope";

    function innerFunction() {
        console.log(outerVariable); 
        // Accessing a variable from the outer scope
    }

    return innerFunction;
}

const closureFunc = outerFunction();
closureFunc();  // Output: "I'm from the outer scope"

Here, innerFunction is a closure because it has access to outerVariable even after outerFunction has finished execution.

2. Practical Use of Closures:

Data Privacy: Closures are often used to emulate private variables in JavaScript. Since the inner function can still access the outer function's variables, it can be used to create data that is private and can only be accessed through a specific function.

Example:

function counter() {
    let count = 0;

    return function() {
        count += 1;
        return count;
    };
}

const increment = counter();
console.log(increment());  // Output: 1
console.log(increment());  // Output: 2
console.log(increment());  // Output: 3

In this example, count is not directly accessible from the outside but can be modified by the returned function.

3. Closures and Loops (Classic Pitfall):

A common problem with closures is when they are created inside loops, leading to unexpected results due to how JavaScript handles closures and variable scoping.

Example of a common mistake:

for (var i = 1; i <= 3; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

Output:

Copy code4
4
4

The reason is that setTimeout creates a closure that refers to the same i variable in memory, and by the time the setTimeout callback runs, i has already become 4.

Fix using an IIFE (Immediately Invoked Function Expression):

for (var i = 1; i <= 3; i++) {
    (function(i) {
        setTimeout(function() {
            console.log(i);
        }, 1000);
    })(i);
}

Output:

Copy code1
2
3

By using an IIFE, a new scope is created for each loop iteration, preserving the correct value of i.

Alternatively, using let solves this issue:

for (let i = 1; i <= 3; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}
ย