DEV Community

Rajat Oberoi
Rajat Oberoi

Posted on • Edited on

JavaScript Main Concepts < In Depth > Part 2

click for Part 1

6. Closures

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).

  • A closure gives you access to an outer function's scope from an inner function.
let b = 3;

function impureFunc(a) {
    return a + b;
}
Enter fullscreen mode Exit fullscreen mode

In order to call a function in our code, JS interpreter needs to know about the function it self and any other data from the surroundings environment that it depends on.
Everything needs to be neatly closed up into a box before it can be fed into the machine.

Image description

A pure function is a function where the output is determined solely by its input values, without observable side effects. Given the same inputs, a pure function will always return the same output.

//Stored in call stack
function pureFunc(a, b) {
    return a + b;
}
Enter fullscreen mode Exit fullscreen mode

Stack Memory:
A: 2
B: 3

Call Stack:

pureFunc(2, 3)

An impure function is a function that interacts with or modifies some state outside its own scope, which means its output can vary even with the same inputs.

  • In below example, in order to interpreter to call this function and also know the value of this free variable, it creates a closure and store them in a place in memory from where they can be access later. That area of memory is called the Heap.
  • Call stack memory is short lived, and heap memory can keep data indefinitely. Later memory gets freed using GC.
  • So a closure is a combination of a function with it's outer state or lexical environment.
  • Closure requires more memory than pure functions.
let b = 3;//free variable

function impureFunc(a) {
    return a + b;
}

Enter fullscreen mode Exit fullscreen mode
  • Impure functions often rely on external state. Closures can encapsulate and manage this state within a function scope, making it possible to create stateful functions without resorting to global variables.
function createCounter() {
    let count = 0; // This is the external state

    return function() {
        count += 1; // Impure function: modifies the external state
        return count;
    };
}

const counter = createCounter();

console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

Enter fullscreen mode Exit fullscreen mode
  • The count variable is encapsulated in the closure created by createCounter.
  • This allows the count variable to persist between function calls, while keeping it private and preventing it from being modified directly from the outside.

So,

  • Closures are often used in JavaScript to create functions with "private" variables or to maintain state across multiple function calls.

Memory Management and Closures:

  • In JavaScript, closures involve storing function references along with their surrounding state. This state is stored in memory, typically in the heap, as it needs to persist beyond the scope of the function execution.
  • When a function with a closure is no longer referenced, the memory it occupies can be garbage collected (GC). However, as long as there are references to the closure, the variables in its scope will not be freed.

Use Case of closure: Memoization

The Fibonacci sequence is a classic example where memoization can significantly improve performance. The naive recursive approach has exponential time complexity due to repeated calculations of the same values.

Naive Recursive Fibonacci Function(inefficient):

function findFabonacciRecursive(number) {
    if (number < 2) {
        return number;
    }
    return findFabonacciRecursive(number - 1) + findFabonacciRecursive(number - 2);
}

console.log(findFabonacciRecursive(10)); // 55

Enter fullscreen mode Exit fullscreen mode

In this approach, the same values of fibonacci(n) are recalculated multiple times, leading to inefficiency. By memoizing the results of the function calls, we can avoid redundant calculations and improve the performance.

Memoized Fibonacci Function:


// Memoized Fibonacci function using a closure
function fibonacciMaster() {
    let cache = {};
    return function fib(n) {
        if (n in cache) {
            return cache[n];
        } else {
            if (n < 2) {
                cache[n] = n;  // Cache the base case result
                return n;
            } else {
                cache[n] = fib(n - 1) + fib(n - 2);
                return cache[n];
            }
        }
    };
}

const fasterFib = fibonacciMaster();
console.log(fasterFib(10)); // 55
console.log(fasterFib(50)); // 12586269025
Enter fullscreen mode Exit fullscreen mode

7. JavaScript Let vs Var vs Const

Using let: It has block scope. So it is not accessible outside of a block i.e. curly braces {}

function start() {
    for(let counter = 0; counter < 5; counter++) {
        console.log(counter);
    }
    //console.log(counter)//ReferenceError: counter is not defined
}

start();
Enter fullscreen mode Exit fullscreen mode

Output:
0
1
2
3
4

Using var: It has function scope. It's scope is not limited to the block in which it is defined but is limited to the function scope.

function start() {
    for(var counter = 0; counter < 5; counter++) {
        console.log(counter);
    }
    console.log(counter)//last value of counter after the for loop ends i.e. value 5
}

start();
Enter fullscreen mode Exit fullscreen mode

Output:
0
1
2
3
4
5

Another example of var:


function start() {
    for(var counter = 0; counter < 5; counter++) {
        if(true) {
            var color = 'red';
        }
    }
    console.log(color)
}

start();
Enter fullscreen mode Exit fullscreen mode
  • When we use var outside of a function, it creates a global variable and attaches that global variable window object in browser. variables declared with let (or const) do not get attached to the global object.

Image description

  • window object is central, suppose we are using a third party library and it has a variable with a same name and so that variable can override our variable. Hence, we should avoid adding stuff to window object. So, Avoid using var keyword.

Key Differences Between var and let:

Scope:

  • var is function-scoped, meaning it is accessible throughout the entire function in which it is declared.
  • let(& const) is block-scoped, meaning it is only accessible within the block (enclosed by {}) where it is declared.

Hoisting: Check 8. headline to understand hoisting further.

  • Variables declared with var are hoisted to the top of their scope and initialized with undefined.
  • Variables declared with let(& const) are also hoisted, but they are not initialized. Accessing them before declaration results in a ReferenceError.

Example Illustrating Scope and Hoisting:


function testVar() {
  console.log(varVar); // Outputs: undefined (due to hoisting)
  var varVar = 'I am var';
  console.log(varVar); // Outputs: 'I am var'
}

function testLet() {
  // console.log(letVar); // Would throw ReferenceError (temporal dead zone)
  let letVar = 'I am let';
  console.log(letVar); // Outputs: 'I am let'
}

testVar();
testLet();

Enter fullscreen mode Exit fullscreen mode

Const keyword:

  • The const keyword in JavaScript is used to declare variables that are constant, meaning their value cannot be reassigned after they are initialized.
  • The binding (the reference to the value) of a const variable cannot be changed, but this does not mean the value itself is immutable. For example, if the value is an object or an array, its properties or elements can still be modified.

const y = 5;
// y = 10; // TypeError: Assignment to constant variable.

const obj = { name: 'Alice' };
obj.name = 'Bob'; // This is allowed
console.log(obj.name); // Outputs: 'Bob'

Enter fullscreen mode Exit fullscreen mode

8. Hoisting

In JavaScript, hoisting is a concept where variable and function declarations are moved to the top of their containing scope during the compilation phase, before the code is executed. This means that regardless of where variables and functions are declared within a scope, they are treated as if they are declared at the top.

Scope in JavaScript refers to the visibility and accessibility of variables, functions, and objects in particular parts of your code during runtime

Variable Hoisting:

  • When variables are declared using var, let, or const, the declaration (not the initialization) is hoisted to the top of the scope.
  • However, only the declaration is hoisted, not the initialization. This means that variables declared with var are initialized with undefined whereas variables declared with let or const are not initialized until the actual line of code where the declaration is made.
  • Variables declared with let or const are hoisted to the top of their block scope, but they are not initialized until their actual declaration is evaluated during runtime. This is known as the "temporal dead zone" (TDZ).
console.log(y);  // ReferenceError: Cannot access 'y' before initialization
let y = 10;
Enter fullscreen mode Exit fullscreen mode

Function Hoisting:

  • Function declarations are completely hoisted, including both the function name and the function body.
  • This allows you to call a function before it is declared in the code.
foo();  // "Hello, I'm John Wick!"

function getName() {
    console.log("Hello, I'm John Wick!");
}

Enter fullscreen mode Exit fullscreen mode
  • Function expressions (functions assigned to variables) are not hoisted in the same way. Only the variable declaration is hoisted, not the function initialization.
getName();  // Error: getName is not a function

var getName = function() {
    console.log("Hello, I'm John Wick!");
};
Enter fullscreen mode Exit fullscreen mode

9. IIFE (Immediately Invoked Function Expression)

An Immediately Invoked Function Expression (IIFE) is a function in JavaScript that runs as soon as it is defined. It is a common JavaScript pattern used to create a private scope and avoid polluting the global namespace.

Here is the basic structure of an IIFE:

(function() {
    // Your code here
})();

Enter fullscreen mode Exit fullscreen mode
  • The function is defined within parentheses () to treat it as an expression, and it is immediately invoked with another set of parentheses ().
(function() {
    console.log("This is an IIFE");
})();

Enter fullscreen mode Exit fullscreen mode

Why Use IIFE?

  • Avoid Global Variables: IIFEs help in avoiding global variables by creating a local scope.
  • Encapsulation: They encapsulate the code, making it self-contained.
  • Immediate Execution: Useful for running setup code for testing on local that should not be run again.

Examples:

With Parameters

(function(a, b) {
    console.log(a + b);
})(5, 10);

Enter fullscreen mode Exit fullscreen mode

Returning Values:


let result = (function() {
    return "Hello, World!";
})();
console.log(result);  // Outputs: Hello, World!

Enter fullscreen mode Exit fullscreen mode

Using Arrow functions:


(() => {
    console.log("This is an IIFE with arrow function");
})();

Enter fullscreen mode Exit fullscreen mode

Q. What does this code log out?

for (var counter = 0; counter < 3; counter++) {
    //Closure as it depends on variable outside of it's scope.
    const log = () => {
        console.log(i);
    }

    setTimeout(log, 100);
}
Enter fullscreen mode Exit fullscreen mode

Output:
3
3
3

  • var has global scope. with var we are mutating over and over again.
  • As we are using a closure, it keeps reference to counter variable in heap memory where it can be used later after timeout is achieved.
  • The time the setTimeout callbacks (the log functions) execute, the counter variable in the outer scope has been incremented to 3. Thus, each log function logs the final value of counter, which is 3.
for (var let = 0; counter < 3; counter++) {
    //Closure as it depends on variable outside of it's scope.
    const log = () => {
        debugger;
        console.log(i);
    }

    setTimeout(log, 100);
}
Enter fullscreen mode Exit fullscreen mode

Output:
0
1
2

  • let is block scope. with let we are creating a variable that is scoped to for loop. i.e. it is local to the for loop and cannot be accessed outside of it.
  • In case of let, closure is capturing the log function with counter variable for each iteration of loop which is 0, 1 and 2.
  • Closure and Execution Context: Because let is block-scoped, each log function captures a unique counter variable from its respective iteration of the loop. Therefore, when each log function executes after 100 milliseconds, it logs the value of counter as it existed at the time of its creation.

Top comments (2)

Collapse
 
jonrandy profile image
Jon Randy 🎖️

Javascript closures are functions that can access values outside of their own curly braces.

This is not correct. Closures aren't functions, and ALL functions can do this. If this definition were correct, there would be no reason to have separate words for 'closure' and 'function'.

Collapse
 
rajatoberoi profile image
Rajat Oberoi • Edited

Hi Jon,

Thank you for pointing out the mistake. I've corrected it.

Thanks,