JavaScript memory management explained

Alright, in order to understand JS memory we need to remember two rules throughout the following article, these are pretty simple:

Primitive types (string, number, boolean) are passed as a copy to function arguments.

(string, number, boolean) Objects are passed as a reference to function arguments

Functions and arrays (and null, but it’s a bug kept for historical reasons and it’s not really mutable as the two others) are considered as Objects by JavaScript

You can just verify that behavior try this typeof [] in your browser console and you’ll end up with object being printed.

How references work in JavaScript and how to interpret it

What you see below is for JavaScript’ engines in general, no matter the framework or environment, but we will see later why this can be tedious with ReactJS and hooks.

const someObject = { a: 5 };

const someArray = [1, 2];

You might be tempted to read the following statement as « someObject is created at { a: 5 } reference location (memory location) » but in fact, you should read it as « someObject variable reference point to the object { a: 5 } reference ».

The above statements can be split as follows

| const someObject | = | { a: 5 };

| const someArray | = | [1, 2]

| variable reference | assignment operator | object Reference

You can also have a look at those two GIST that add more explanations: This one and this one.

Ok now we have the basics, let’s see why this can be problematic if badly understood.

JavaScript closures and references

A quick remember about closures

A closure is a function that remembers its outer variables and can access them. In JavaScript, almost all functions are naturally closures.

While using JavaScript you’re using closure and managing scopes every time and by doing this you need to understand how references work.

Within closure, there is a notion of Lexical Environment which hold values of different scopes. I won’t develop that much here if you want to know more, feel free to read details here.

➡️ Primitive types with arguments are passed as a copy. What’s following is a quick example.

In the below example, modifying a or b has no side effects at all arg within scope 2. Also modifying arg within scope 2 has no effect on whether a or b .

Remember: scopes are delimited within brackets { } .

// Scope 1

let a = 5;

let b = "hello"; function test(arg) {

// Scope 2

arg = "Something else";

} test(a)

test(b)

➡️ Objects are passed as reference, we will study that through closure with mutations and assignation.

Assignation vs Mutation with closure in JavaScript

As closure and callback are the core of JavaScript programming and event-driven programming you will much time rely on such things.

In the following example, there is a quick NodeJS snippet, which will mimic the behavior of displaying messages to users that are registered to a topic. But this code is a bit dynamic:

During the program execution, the subscribed users will change. I’m mimicking this using a setInterval function every 10 seconds . Sometimes one interest user is added, sometimes one is removed.

function . Sometimes one interest user is added, sometimes one is removed. Every second, we send a random message to subscribed users.

At the end of the file, there are two setInterval functions that are responsible for mutating our array of subscribed users. We also created a variable scope thank to closure function called startListeningForMessages .

Let’s try the code

Feel free to use your own NodeJS environment or this one hosted in the cloud: https://repl.it/languages/nodejs

Script usage description

Try to start the program with the first setInterval uncommented, then invert and uncomment the second one and comment the first.

Do you see the difference?

Explanation

In the first interval , nothing shows. That’s because we are replacing the reference of our array. Even if passed as reference listeningUsers argument still point to the old interestedUsers pointer reference. That way interestedUsers is getting reassigned while the old interestedUsers still exists but can’t be garbage collected as reference still exists nor accessed outside of the closure scope. This could be a memory leak, but in our case, it’s even worse because we are not synchronized anymore in our interest list.

, nothing shows. That’s because we are replacing the reference of our array. Even if passed as reference argument still point to the old pointer reference. That way is getting reassigned while the old still exists but can’t be garbage collected as reference still exists nor accessed outside of the closure scope. This could be a memory leak, but in our case, it’s even worse because we are not synchronized anymore in our interest list. In the second interval, things change. That’s because we are using mutating methods from the Array object. That way the reference is not changing but elements inside the array are. By doing so we are not losing the context through our closure function.

That’s why you should always ask yourself whether you should mutate or reassign something and think about side effects while using out of scope variables.

This also applies to objects!

For an object that’s pretty much the same, if you edit a key of the object inside the closure function, it’s okay as you still point to the same reference. But if you re-assign the object inside the function, you lose the reference.

What about React hooks?

Same thing here, as useState will only cause re-render if Object.is comparison returns false after calling the setter if you use useState and don’t change the reference your UI will not get updated.

That’s why generally we ask for useEffect and clean closure scope on state change, but sometimes by doing so, during the timeframe of unsubscribe/resubscribe with new values you might lose some events from the event listener. Also its really important when having object state or array state