Closures vs WeakMaps: Hiding Data in JavaScript

Peter BamigboyePeter Bamigboye
7 min read

In our previous article in the series here, I got a comment on a code snippet I used which made use of closures which reminded me that it is an often confusing part of JavaScript, which then inspired this article.

JavaScript developers often face the challenge of encapsulating or “hiding” data so that it's not directly accessible or modifiable from the outside. Two powerful mechanisms to achieve data encapsulation are closures and WeakMaps. While both techniques help in hiding data, they work very differently under the hood. In this article, we’ll explore how each one operates and their nuanced differences—perfect for developers who may be new to these concepts.

1. Using Closures for Data Hiding

A closure is created when a function is defined inside another function, allowing the inner function to access the variables of the outer function even after the outer function has finished executing.

How Closures Work

When you declare a variable inside a function, it is normally not accessible from the outside. However, if you return an inner function that uses this variable, the variable “persists” due to the closure. This gives you the ability to hide state and only expose methods that operate on that state.

Example: Creating a Private Counter

function createCounter() {
  // 'count' is a private variable, hidden in the closure.
  let count = 0;

  // Returning an object exposing public methods that have access to 'count'
  return {
    increment() {
      count++;
      console.log('Incremented count:', count);
    },
    getCount() {
      return count;
    },
  };
}

const counter = createCounter();
counter.increment(); // Output: Incremented count: 1
console.log(counter.getCount()); // Output: 1
// 'count' is not directly accessible from outside:
console.log(counter.count); // Undefined

Step-by-Step Explanation of the Closure Example

  1. Private State:
    Inside createCounter(), we declare let count = 0; which is completely hidden from the global scope.

  2. Public Methods:
    The returned object has methods increment and getCount. These functions form a closure—they “remember” the environment in which they were created, i.e., the variable count.

  3. Encapsulation:
    Since count is not a property of the returned object, it is truly private. The only way to change or read its value is by calling the exposed methods.

Closures provide a straightforward method to encapsulate data in a functional style, ensuring that internal state is only accessible through deliberate API methods.

2. Using WeakMaps for Data Hiding

WeakMaps allow you to associate data with an object without exposing that data as a property on the object. They are called “weak” because they do not prevent garbage collection—once there are no other references to the key object, the entry in the WeakMap can be cleaned up by the garbage collector.

How WeakMaps Work

WeakMaps are collections where keys must be objects and values can be arbitrary. By storing private data in a WeakMap keyed by an object, you can hide that data from direct access.

Example: Attaching Private Data to an Object

const privateData = new WeakMap();

class Person {
  constructor(name) {
    // The instance object 'this' serves as a key to store private data
    privateData.set(this, { name });
  }

  getName() {
    // Retrieve private data using the instance (this) as the key
    const data = privateData.get(this);
    return data.name;
  }
}

const alice = new Person("Alice");
console.log(alice.getName()); // Output: Alice
// The private data is hidden from direct access:
console.log(alice.name); // Undefined

Step-by-Step Explanation of the WeakMap Example

  1. Private Data Storage:
    A new WeakMap named privateData is created. This map will serve to store private information associated with objects.

  2. Association via Key:
    In the Person constructor, the instance (this) is used as a key in the WeakMap to store an object containing the private property name.

  3. Data Access Methods:
    The getName method retrieves the private data by looking up this in the WeakMap. Since the name is not stored as a direct property on the instance, it remains hidden from the outside.

  4. Memory Efficiency:
    The weak reference in WeakMap means that if an object (e.g., a Person instance) is no longer referenced elsewhere, its associated private data can be garbage collected, preventing memory leaks.

Example 2: Tracking Access Timestamps for DOM Elements

Imagine you're building a tooltip system. You want to record the last time each tooltip-enabled element was hovered over. You don’t want to store this info directly on the DOM elements, and you also want the data to disappear when the DOM element is removed—so, no memory leaks.

A WeakMap fits perfectly here.

const hoverTimestamps = new WeakMap();

// Assume these are DOM elements on your page
const button = document.querySelector("#save-button");
const card = document.querySelector("#profile-card");

function trackHover(element) {
  element.addEventListener("mouseenter", () => {
    hoverTimestamps.set(element, new Date());
    console.log(`Hovered on ${element.id} at`, hoverTimestamps.get(element));
  });
}

trackHover(button);
trackHover(card);

Step-by-Step Explanation

  1. Creating a WeakMap:

     const hoverTimestamps = new WeakMap();
    
    • A new WeakMap is created and assigned to the variable hoverTimestamps. This map will be used to store the hover timestamps associated with the DOM elements.
  2. Selecting DOM Elements:

     const button = document.querySelector("#save-button");
     const card = document.querySelector("#profile-card");
    
    • The document.querySelector() method is used to select two elements from the DOM: the button with the ID save-button and the card with the ID profile-card. These elements are assigned to the variables button and card respectively.
  3. Defining the trackHover Function:

     function trackHover(element) {
       element.addEventListener("mouseenter", () => {
         hoverTimestamps.set(element, new Date());
         console.log(`Hovered on ${element.id} at`, hoverTimestamps.get(element));
       });
     }
    
    • The function trackHover is defined to track the hover event on any DOM element passed as the element parameter.

    • Inside this function:

      • element.addEventListener("mouseenter", ...): An event listener is attached to the element that listens for the "mouseenter" event, which is fired when the mouse enters the element.

      • When the mouse enters the element, the callback function is executed:

        • hoverTimestamps.set(element, new Date()): The current date and time are captured using new Date() and stored in the hoverTimestamps WeakMap, using the element as the key.

        • console.log(...): This logs the ID of the element (using element.id) and the stored timestamp (retrieved with hoverTimestamps.get(element)) to the console.

  4. Applying the trackHover Function:

     trackHover(button);
     trackHover(card);
    
    • The trackHover function is called twice—once for the button element and once for the card element.

    • This sets up the hover tracking for both elements. When either of them is hovered over, the current timestamp is recorded in the WeakMap and logged to the console.

WeakMaps are particularly useful in scenarios where you want to associate private data with objects while keeping those associations hidden and ensuring memory is managed efficiently.

3. Comparing Closures and WeakMaps

AspectClosuresWeakMaps
UsageEncapsulate variables within function scopeAttach private data to objects without exposing it
Data AssociationData is tied to the function’s lexical scopeData is tied to an object (as a key in the map)
Garbage CollectionVariables persist as long as the closure existsEntries are garbage collected when the key is lost
Memory EfficiencyCan potentially retain large closures if not managedMore efficient for many objects due to weak references
Syntax & StyleFunctional approach; simple to implementObject-oriented; works well with classes

4. When to Use Each Approach

  • Choose Closures When:

    • You are building a module, library, or function-based component where state encapsulation is needed.

    • You prefer a functional style and the encapsulated data is limited to the function’s context.

  • Choose WeakMaps When:

    • You are working with classes or objects and want to attach private data without altering the object’s public interface.

    • You need automatic memory management for private associations without risking memory leaks.

Conclusion

Both closures and WeakMaps provide powerful ways to hide data in JavaScript, and understanding their differences is key to building secure and maintainable code.

  • Closures give you a simple, functional way to encapsulate state, keeping variables private within a function’s scope.

  • WeakMaps allow you to attach private data directly to objects, ensuring that the data isn’t exposed as public properties and is efficiently garbage collected.

This article is part of the Nuances in Web Development series, where we uncover subtle yet significant differences in how you can approach common challenges, you can check out previous articles here. Make sure you subscribe to our newsletter and stay tuned for our next deep dive into the intricacies of modern web development!

21
Subscribe to my newsletter

Read articles from Peter Bamigboye directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Peter Bamigboye
Peter Bamigboye

I am Peter, a front-end web developer who writes on current technologies that I'm learning or technologies that I feel the need to simplify.