Closures vs WeakMaps: Hiding Data in JavaScript

Table of contents

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
Private State:
InsidecreateCounter()
, we declarelet count = 0;
which is completely hidden from the global scope.Public Methods:
The returned object has methodsincrement
andgetCount
. These functions form a closure—they “remember” the environment in which they were created, i.e., the variablecount
.Encapsulation:
Sincecount
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
Private Data Storage:
A new WeakMap namedprivateData
is created. This map will serve to store private information associated with objects.Association via Key:
In thePerson
constructor, the instance (this
) is used as a key in the WeakMap to store an object containing the private propertyname
.Data Access Methods:
ThegetName
method retrieves the private data by looking upthis
in the WeakMap. Since the name is not stored as a direct property on the instance, it remains hidden from the outside.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
Creating a WeakMap:
const hoverTimestamps = new WeakMap();
- A new
WeakMap
is created and assigned to the variablehoverTimestamps
. This map will be used to store the hover timestamps associated with the DOM elements.
- A new
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: thebutton
with the IDsave-button
and thecard
with the IDprofile-card
. These elements are assigned to the variablesbutton
andcard
respectively.
- The
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 theelement
parameter.Inside this function:
element.addEventListener("mouseenter", ...)
: An event listener is attached to theelement
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 usingnew Date()
and stored in thehoverTimestamps
WeakMap
, using theelement
as the key.console.log(...)
: This logs the ID of the element (usingelement.id
) and the stored timestamp (retrieved withhoverTimestamps.get(element)
) to the console.
Applying the
trackHover
Function:trackHover(button); trackHover(card);
The
trackHover
function is called twice—once for thebutton
element and once for thecard
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
Aspect | Closures | WeakMaps |
Usage | Encapsulate variables within function scope | Attach private data to objects without exposing it |
Data Association | Data is tied to the function’s lexical scope | Data is tied to an object (as a key in the map) |
Garbage Collection | Variables persist as long as the closure exists | Entries are garbage collected when the key is lost |
Memory Efficiency | Can potentially retain large closures if not managed | More efficient for many objects due to weak references |
Syntax & Style | Functional approach; simple to implement | Object-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!
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.