Week 6 - Advanced topics

Synchronous File IO

File processing

Error handling

Array methods

Closures

Style - How to write meaningful comments

Practice

Assignment

Core program

What are Closures?

A closure is a function that "remembers" variables from its outer scope, even after that outer function has finished executing.

This might sound confusing at first, so let's break it down with an analogy. Imagine you're at a coffee shop and you order a custom drink. The barista writes your order on a cup and hands it to another barista to make. Even after you've left the counter and the first barista has moved on to other customers, the second barista can still read your order from the cup and make your drink correctly. Closures work similarly: a function can carry information from where it was created and use it later, even after the original context is gone.

https://www.youtube.com/watch?v=beZfCfiuIkA

function createGreeter(greeting) {
  // This variable is "closed over" by the inner function
  return function(name) {
    return `${greeting}, ${name}!`;
  };
}

const sayHello = createGreeter('Hello');
const sayHi = createGreeter('Hi');

console.log(sayHello('Alice')); // Hello, Alice!
console.log(sayHi('Bob'));      // Hi, Bob!

Let's walk through what's happening:

  1. We call createGreeter('Hello') which creates a function and returns it
  2. That returned function gets stored in sayHello
  3. Even though createGreeter has finished running, the function in sayHello still remembers that greeting was 'Hello'
  4. Later, when we call sayHello('Alice'), it can still access that greeting variable

The inner function has access to greeting even though createGreeter already finished running.

That's a closure.

You've actually been using closures without realizing it. Every time you write a function inside another function, you're creating a closure. What makes them powerful is this ability to preserve access to variables from the outer function.

<aside> 💡

Every function in JavaScript creates a closure. The function remembers the variables that existed when it was created, not just what's passed as parameters.

</aside>

Why Closures Matter

Closures let you:

// Without closure - needs global variable
let count = 0;
function increment() {
  count++;
  return count;
}

// With closure - private variable
function createCounter() {
  let count = 0;
  return function() {
    count++;
    return count;
  };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// count is not accessible from outside

Examples

Example 1: Simple Closure

function outer() {
  const message = 'Hello from outer';
  
  function inner() {
    console.log(message); // Can access outer's variable
  }
  
  return inner;
}

const myFunction = outer();
myFunction(); // Hello from outer

The inner function closes over the message variable.

Example 2: Multiple Closures

// Regular function syntax
function createMultiplier(multiplier) {
  return function(number) {
    return number * multiplier;
  };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5));  // 10
console.log(triple(5));  // 15

Each function remembers its own multiplier value. These are examples of factory functions: functions that create and return other functions, each with their own private data. The double function permanently remembers multiplier = 2, while triple remembers multiplier = 3, even though they came from the same factory.

// Arrow function syntax
const createMultiplier = (multiplier) => (number) => number * multiplier;

const double = createMultiplier(2);
console.log(double(5)); // 10

This compact syntax returns a function that returns a function. This pattern is sometimes called currying: transforming a function with multiple parameters into a sequence of functions that each take one parameter.

<aside> ⌨️

Hands on: Create a createTemperatureConverter() function that takes a unit ('C' or 'F') and returns a converter function. For example, createTemperatureConverter('C') should return a function that converts Fahrenheit to Celsius. Test with both converters.

</aside>

Example 3: Private Variables

function createBankAccount(initialBalance) {
  let balance = initialBalance; // Private variable
  
  return {
    deposit(amount) {
      balance += amount;
      return balance;
    },
    withdraw(amount) {
      if (amount > balance) {
        return 'Insufficient funds';
      }
      balance -= amount;
      return balance;
    },
    getBalance() {
      return balance;
    }
  };
}

const account = createBankAccount(100);
console.log(account.getBalance()); // 100
account.deposit(50);
console.log(account.getBalance()); // 150
console.log(account.balance);      // undefined - private!

The balance variable is completely private. No way to access it except through the provided methods.

Example 4: Configuration with Closures

function createLogger(prefix) {
  return function(message) {
    console.log(`[${prefix}] ${message}`);
  };
}

const errorLog = createLogger('ERROR');
const infoLog = createLogger('INFO');

errorLog('Something went wrong');  // [ERROR] Something went wrong
infoLog('App started');            // [INFO] App started

<aside> ⌨️

Hands on: Create a createTimer() function that returns an object with start(), stop(), and getElapsed() methods. Use closures to keep the start time private. Test it by starting the timer, waiting a few seconds, stopping it, and checking the elapsed time.

</aside>

Common Pitfalls

There are a few common pitfalls to consider while working with closures.

Accidental Shared State

// Problem: All counters share the same count variable
function createCounter() {
  return {
    count: 0,
    increment() {
      this.count++;
      return this.count;
    }
  };
}

const counter1 = createCounter();
const counter2 = createCounter();
console.log(counter1.increment()); // 1
console.log(counter2.increment()); // 1 - separate instances ✓

// Each counter has its own count - no shared state

While closures prevent external access to variables, they don't automatically prevent shared state between multiple instances. In the example above, each call to createCounter() creates a completely new object with its own count property, so there's no sharing. However, if you're not careful about where you define your variables, you might accidentally create shared state across multiple closures. Always ensure that variables you want to be private and independent are defined inside the factory function, not outside it.

Memory Leaks

// Be careful with closures in loops or event handlers
function attachHandlers() {
  const largeData = new Array(1000000).fill('data');
  
  document.querySelector('#button').addEventListener('click', function() {
    // This closure keeps largeData in memory even if not used!
    console.log('Clicked');
  });
}

// Better: Only close over what you need
function attachHandlers() {
  const largeData = new Array(1000000).fill('data');
  const summary = largeData.length; // Extract only what's needed
  
  document.querySelector('#button').addEventListener('click', function() {
    console.log(`Data size: ${summary}`);
    // largeData can now be garbage collected
  });
}

Closures keep variables alive in memory as long as the function exists. This is normally fine, but can cause memory leaks if you're not careful. In the first example, the event handler closure keeps the entire largeData array in memory forever, even though it never uses it. The browser can't garbage collect that array because the closure maintains a reference to it. The solution is to only close over the specific data you need: extract just the length or other small values before creating the closure, allowing the large data structure to be freed from memory.

Additional Resources

Video

Reading


CC BY-NC-SA 4.0 Icons

*https://hackyourfuture.net/*

Found a mistake or have a suggestion? Let us know in the feedback form.