Hi, I am Sanjeet Tiwari...
Let's talk about

Back to Notes

Scope & Closure

JavaScript

Scope of a variable is the area of the code where it is accessible.

First, we need to understand what an execution context is.

Whenever a function is invoked in JS, an execution context is created which has 2 parts -

After creation, the execution context is then pushed onto the call stack. While execution, a Global Execution Context will already be present in the call stack, which has nothing but the window object in its memory.

Lexical Environment

A Lexical Environment of any variable or function in JS is the memory of its execution context + the lexical environment of its parent.

Whenever an execution context is created, a reference in its memory is also created which points to its parent’s memory.

Scope Chain

Whenever a variable is accessed in JS, the JS engine first tries to find it in the variable’s execution context, and if unable to find, the JS engine jumps on to the execution context of it’s parent.

This process repeats until either the variable is found somewhere, or when JS engine tries to reference Global Execution Context’s parent which is null.

Types of Scope

There are 5 different types of scope which appear under Scope in dev tools whenever any execution context is selected in the call stack.

  1. Block
  2. Local
  3. Global
  4. Script
  5. Module

Script vs Module

The 2 most confusing scopes are Script and Module. Lets consider this scenario -

// script1.js
let a = "Welcome"

// script2.js
console.log(a)

// In index.html
<script src="script1.js">
<script src="script2.js">

Then the a will be printed in the console, and a will appear under scope Script.

If we make script1 js script an ES6 module like this -

<script src="script1.js" type="module">

Then the main script execution will throw a Reference error stating a is not defined. It’s because a is now inside Module scope, and is not accessible outside the module.

Difference between let, const and var

In terms of scope, variables initialized using let and const reside in a different memory space than variables initialized using var.

Let’s take the following example -

console.log(a) // this will give a Reference error
let a = 10
var b = 20

the variable a will be seen under Script scope and variable b will be seen under Global scope, as all variables declared using var get attached to global object.

It’s the same reason why we have something called a temporal dead zone for let, const variables whenever they are accessed before initialization.

Closures

A Closure is a function bundled together with its lexical environment.

In simple terms, whatever memory that a function can access via scope chain, other than its own is called its closure.

Let’s take an example code -

function a() {
    let a = 10
    function b() {
        console.log(a) // if a debugger is applied here
    }
    b()
}

a()

When debugger is applied, we’ll be able to see a inside Closure scope of execution context of b().

Let’s take a more complex example -

function a() {
    let a = 10
    return function b() {
        console.log(a)
    }
}

const x = a()

x()

Here, even though the execution context of a has been popped out of the call stack, it will still return the value that was carried by a.

Whenever a function is returned in a JS, it not only returns the function, but also it’s lexical scope along with it, so that whenever it’s executed later in the code, it can refer to previously existed variables.

Hence, an entire closure is returned.

The copy of the lexical scope that is returned alongside the function holds live copies of variables, as references are returned, not actual values.

Applications of Closures

Function Currying

Currying is a process of converting a function f(x, y, z) in a format via which it can be invoked by passing arguments in a sequence pattern - f(x)(y)(z).

Let’s use closures to write it’s code -

function curryAFunction(f) {
    if (typeof f !== "function") throw new Error("Only functions can be passed as arguments")
    // we have a function
    // lets setup a recursive function to convert it into a curry function
    return function curried(...args) {
        // this function will keep on receiving new args (2)(4)...(n)
        // we need to check whether all args have been accounted for or not
        if (f.length <= args.length) {
            // all args have been utilized
            // just call the function normally to return the result
            return f(...args);
        }
        // some arguments are left to come
        // return one more function that will take the next arg
        return (...args2) => {
            // now curried will be called with both the arguments together
            const newArgs = [...args, ...args2];
            return curried.bind(null, ...newArgs);
        }
    }
}

Debounce

The idea of debounce is to delay a function call until a certain amount of time has passed since the last action.

function debounce(cb, delay = 1000) {
    // cb is the callback which will be debounced
    // initialize the timer variable
    let timeout

    return (...args) => {
        // whenever this function is invoked
        // create a new setTimeout and clear the old timeout
        if (timeout) clearTimeout(timeout)
        timeout = setTimeout(() => {
            // callback will get executed only when delay has passed 
            // since last function invocation
            cb(...args)
        }, delay)
    }
}

Debounce is essential where ever we want to reduce invocations of event handlers which may perform heavy tasks like calling an API which we can’t do frequently. A good use case is the search autocomplete.

Throttle

Since the delay in debounce can be very long if the data is frequently changing all the time. Throttle enables the callback/function to get called every fixed delay whenever there is continuous input.

function throttle(cb, delay = 1000) {
    // need to know when the timer is still running and we should wait for
    // callback invocation
    let shouldWait = false

    // these are the args with which the function was called when timer is still running
    // need to keep these as there always be a last setTimeout which will execute but not call
    // the callback with latest values
    let waitingArgs = null

    const timerFunc = () => {
        if (waitingArgs) {
            cb(...waitingArgs)
            waitingArgs = null
            // reset the timer because shouldWait is not turning false here
            setTimeout(timerFunc, delay);
        } else {
            shouldWait = false
        }
    }
    
    return (...args) => {
        if (shouldWait) {
            waitingArgs = args
            return
        }
        
        // call the callback if we should not wait
        cb(...args)
        waitingArgs = null
        shouldWait = true

        // setting a timer
        setTimeout(timerFunc, delay)
    }
}

Throttle is used in scenarios where data is rapidly changing and we can’t wait longer for accessing the current data. An example would be to track the movement of the cursor on the page and sending that data to the server.

Video Sources

The Scope Chain, 🔥Scope & Lexical Environment | Namaste JavaScript Ep. 7

The Scope Chain, 🔥Scope & Lexical Environment | Namaste JavaScript Ep. 7

Akshay Saini

If You Cannot Name All 5 JS Scopes You Need To Watch This Video

If You Cannot Name All 5 JS Scopes You Need To Watch This Video

Web Dev Simplified

Closures in JS 🔥 | Namaste JavaScript Episode 10

Closures in JS 🔥 | Namaste JavaScript Episode 10

Akshay Saini

Learn Debounce And Throttle In 16 Minutes

Learn Debounce And Throttle In 16 Minutes

Web Dev Simplified

Last updated on 31-07-2024