Skip to content

Unraveling the mysteries of function closures in Javascript and Typescript

Posted on:November 26, 2023

Hey ninjas 🥷! If you’ve ever come across the concept of function closures, you know it packs quite a punch in the programming dojo. Think of it as a hidden technique that, once mastered, can make your code more efficient and encapsulated. Today, we’ll step into the dojo together to learn about function closures - what they are, their benefits, and how to use them to our advantage.

The essence of function closures

What exactly are function closures?

A function closure occurs when an inner function retains access to its outer function’s scope, even after the outer function completes its execution. It’s as if the inner function holds onto the knowledge 🧠 of its parent function like a deep-rooted tree 🌳 keeping the wisdom of the earth even after seasons change.

A basic example:

function outerFunction(x: number) {
  return function innerFunction(y: number) {
    return x + y;
  };
}

const addTwo = outerFunction(2);

console.log(addTwo(5)); // Output: 7

Here, outerFunction bequeaths to innerFunction the ability to remember x. It’s like bequeathing an enduring legacy that outlives the original context in which it was established.

Advantages of wielding function closures

  1. Data encapsulation: Closures keep your data tucked away securely 🔒, accessible only to the functions that need to know 🔑, akin to a hidden compartment in a ninja’s toolkit.
  2. Clean global namespace: By using closures, you prevent littering the global scope with unnecessary variables, much like how a disciplined martial artist maintains a clutter-free training space 🧘.
  3. Enabling partial applications: Through closures, you get the flexibility of setting parameters beforehand, akin to an archer 🏹 nailing a bullseye by adjusting the bowstring tension in advance.

Armed with these benefits, we can delve deeper into creating our own closures in JS/TS.

Crafting function closures in JS/TS

Concocting a counter closure

function counter() {
  let count = 0;

  return function increment() {
    return ++count;
  };
}

const incrementCount = counter();

console.log(incrementCount()); // Output: 1
console.log(incrementCount()); // Output: 2
console.log(incrementCount()); // Output: 3

The counter function spawns an increment function with a secret - the count variable. This example illustrates how a closure shields the count variable, like a hidden fortress 🏯, only visible to those within its walls - in this case, our increment function.

Harnessing memoization with closures

function memoize() {
  const cache: { [key: string]: number } = {};

  return function calculate(input: string) {
    if (input in cache) {
      console.log("Fetching from cache");

      return cache[input];
    }

    const result = Number(input) ** 2;

    cache[input] = result;

    return result;
  };
}

const squareValue = memoize();

console.log(squareValue("2")); // Output: 4
console.log(squareValue("4")); // Output: 16
console.log(squareValue("2")); // Output: 4 (Fetching from cache)

In the above memoization technique, the calculate function within memoize creates a closure so that its cache remains accessible across calls, allowing it to retrieve and store results like a meticulous librarian.

The philosophy of lexical scoping in closures

Lexical scoping refers to the idea that a function’s scope is determined at the time of its definition, not its execution. It’s like an ancestral legacy passed down to an inner function, which can then use the outer function’s resources seamlessly.

Function closures in action

  1. Event listeners: Like devoted guardians, closures retain the relevant outer scope variables needed to manage UI events reliably and efficiently.
    function activateDavinciSecretMode(selector: string) {
      const body = document.querySelector("body");
      const elements = document.querySelectorAll(selector);
    
      function applyDaviciSecretMode() {
        Array.from(elements).forEach((element) => {
          element.style.transform = "rotateY(180deg)";
          element.style.fontStyle = "italic";
        });
    
        body.removeEventListener("click", applyDaviciSecretMode);
      }
    
      body.addEventListener("click", applyDaviciSecretMode);
    }
    
    activateDavinciSecretMode("body > *");

    This applyDavinciSecretMode closure remembers the elements from when activateDavinciSecretMode was called, even when the click event happens much later.

  2. Encapsulation of class members: Within classes, closures become the watchful protectors of data, ensuring class members remain private and under strict surveillance.
    class SecretKeeper {
      #secret = '';
      
      constructor(secret: string) {
        this.#secret = secret;
      }
      
      getRevealSecret() {
        return () => this.#secret;
      }
    }
    
    const secretKeeper = new SecretKeeper('💰');
    const getSecret = secretKeeper.getRevealSecret();
    
    console.log(getSecret()) // '💰'

    The returned function from getRevealSecret() maintains a closure over this.#secret, allowing access only through the specifically designed getSecret function.

  3. Currying and specialization: Tailored like a custom-fit garment, closures allow for pre-configuring parameters, sharpening the specificity of our functions.
    const power = (a: number) => (b: number) => b ** a;
    const square = power(2);
    const cube = power(3);
    
    console.log(square(2)) // 4
    console.log(square(3)) // 9
    console.log(square(4)) // 16
    
    console.log(cube(2)) // 8
    console.log(cube(3)) // 27
    console.log(cube(4)) // 64

    Here, square and cube are like a personalized strike, where power is the style of attack 🥋, and square and cube are the specific moves you deploy in the battle field 👊.

Mastering the art of function closures with best practices

Best practices for harnessing function closures

Enhancing performance and cleanliness

Conclusion:

We’ve explored the shadows and emerged with a clearer understanding of function closures in Javascript and TypeScript. Like any skillful practitioner, we know our code’s strength lies in elegance and thoughtful application of techniques.

Embrace these insights in your Javascript and TypeScript adventures, stay adaptable and continue honing your craft - remain as adaptable as water, shaping your code fluidly and harmoniously. Happy coding! 💻

Review:

What is a function closure in programming?

A function closure occurs when an inner function retains access to its outer function’s scope, that is its creation environment, even after the outer function completes its execution, allowing it to utilize variables from its parent function even post execution.

Provide a basic TypeScript example of a function closure.

Here is a basic example of a function closure in TypeScript:

function outerFunction(x: number) {
  return function innerFunction(y: number) {
    return x + y;
  };
}

const addTwo = outerFunction(2);

console.log(addTwo(5)); // Output: 7
Enumerate three advantages of using function closures.

Advantages of function closures include:

  1. Data encapsulation: Closures keep data secure and accessible only to the functions that need to know.
  2. Clean global namespace: Closures help avoid cluttering the global scope with unnecessary variables.
  3. Enabling partial applications: Closures allow for setting parameters beforehand.
How do you create function closures?
  1. Construct an outer function.
  2. Declare essential variables within this function.
  3. Return an inner function that references those variables, thereby forming a closure.
How does memoization utilize closures in TypeScript?

Memoization utilizes closures to retain access to a cache object across multiple function calls. Here is an example in TypeScript:

function memoize() {
  const cache: { [key: string]: number } = {};

  return function calculate(input: string) {
    if (input in cache) {
      console.log("Fetching from cache");

      return cache[input];
    }

    const result = Number(input) ** 2;

    cache[input] = result;

    return result;
  };
}

const squareValue = memoize();

console.log(squareValue("2")); // Output: 4
console.log(squareValue("4")); // Output: 16
console.log(squareValue("2")); // Output: 4 (Fetching from cache)
What is lexical scoping in the context of closures?

Lexical scoping refers to a function’s scope being determined at the time of its definition, not its execution, allowing inner functions to use the outer function’s resources seamlessly, even after the outer function has terminated.

What are some common use cases for closures?

Closures excel in scenarios like event listener management, class member encapsulation, and functions requiring currying or partial application techniques.

How are closures used to encapsulate class members in TypeScript?

Here’s how closures encapsulate class members in TypeScript:

class SecretKeeper {
  #secret = '';
  
  constructor(secret: string) {
    this.#secret = secret;
  }
  
  getRevealSecret() {
    return () => this.#secret;
  }
}

const secretKeeper = new SecretKeeper('💰');
const getSecret = secretKeeper.getRevealSecret();

console.log(getSecret()) // '💰'

The returned function from getRevealSecret() maintains a closure over this.#secret, keeping it encapsulated.

How do closures enable currying and specialization?

Closures enable currying and specialization by allowing for pre-configuring parameters. An example in TypeScript can be:

const power = (a: number) => (b: number) => b ** a;
const square = power(2);
const cube = power(3);

console.log(square(2)) // 4
console.log(square(3)) // 9
console.log(square(4)) // 16

console.log(cube(2)) // 8
console.log(cube(3)) // 27
console.log(cube(4)) // 64
Mention two best practices for working with function closures.

Best practices for function closures include:

  • Memory awareness: Be cautious with closures in lasting scopes to avoid memory leaks.
  • Mutable variables: Avoid unpredictable outcomes by using immutable data or having explicit agreements between closures.
How does restraint in applying cautionary measures improve efficiency and cleanliness when working with closures?

Efficiency and cleanliness can be improved with closures by:

  • Avoiding unnecessary closures to prevent code bloat.
  • Using currying effectively without creating a maze of overly specialized functions.
  • Naming closures meaningfully for better readability and maintainability.