In JavaScript, the memory taken by the values held by the variables is automatically freed when they are not used anymore. This is done by the garbage collector to free up space. How can we benefit from knowing how it works and therefore how to write better code? Let’s find out!
Memory management
In more classic languages like C, we need to manage the memory. After we allocate it and use it, we need to release the memory when we don’t need it anymore to free it up. This is done explicitly by the developer. In JavaScript, both allocation and release of the memory happen under the hood.
Allocating the memory
In JavaScript, you allocate the memory while declaring variables:
1 2 3 4 5 6 7 8 9 |
const age = 30; const dogName = 'Fluffy'; const dog = { name: 'Fluffy' }; function getDogName(dog) { return dog.name; } |
This includes declaring a function. The specific way of allocating the memory is engine specific and might differ between different implementations (for example used in Chrome and Firefox).
Releasing the memory
After allocating the memory, you can read and write to it. At some point, you might not need it anymore, though. In languages in which you are responsible for the memory management, you should free the space that is no longer needed. In JavaScript, all that is done, by the garbage collector.
Garbage collector
Garbage collection is a process of finding a memory that is no longer used and releasing it. This is done by the garbage collector. It is not obvious which parts of the memory should be freed. To decide which should be kept and which to free, the garbage collector can implement a certain algorithm.
Reference-counting garbage collection
Back in the day engines used a very simple algorithm for deciding if the object is not needed anymore. In this strategy, an object is considered garbage collectable if there are no references pointing to it, which is the simplest garbage collection algorithm.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
let john = { name: 'John' } const dog = { name: 'Fluffy', owner: john } const cat = { name: 'Garfield', owner: john } |
In the code above, there are three objects: a dog, a cat and john. John is shared between this two pets and is saved in the memory. Overall, there are three places holding a reference to the owner: a dog, cat and a variable named john.
1 2 3 4 5 6 7 8 9 10 |
dog.owner = { name: 'Bob' } // two references remaining john = null; // one references remaining cat.owner = dog.owner; // no references remaining |
After running the code above, there are no references left to the original object that was created, and therefore, it can be garbage collected.
This approach has some limitations. Imagine a situation like that:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
const dog = { name: 'Fluffy', owner: { name: 'John' } } const cat = { name: 'Garfield', owner: { name: 'Bob' } } dog.owner.friend = cat.owner; cat.owner.friend = dog.owner; dog.owner = null; cat.owner = null; |
In this code there is a cycle: both dog.owner and cat.owner have references to each other. Even after removing them from the pets, they still have references pointing at them and they will not be removed. This is an example of a memory leak: a situation in which the memory is not released even though it is no longer used.
Mark-and-sweep algorithm
Since 2012 all modern browsers come with a mark-and-sweep garbage collecting algorithm.
While deciding if the memory should be cleared, it does not examine if the object is not needed anymore. Instead, it checks if the object is not reachable by any means. It traverses the list of all variables in the environment starting from the global scope and marks all the reachable memory as active. It follows all the references that it encounters in all available children scopes. If by the end of the end some piece of the memory was not marked, it is considered garbage and can be swept.
Thanks to that improvement, our example from above would not be problematic anymore. If the objects have references to each other but can’t be reached in any way, the memory they have been given can be freed.
Optimizing the code
Knowing all that, we can try to optimize our code. Consider this:
1 2 3 4 5 6 7 |
const lotsOfData = localStorage.getItem('lotsOfData'); processData(lotsOfData); const button = document.querySelector('#button'); button.addEventListener('click', () => { console.log('Doing something else'); }); |
The user might click the button much later after the code was executed. The catch with this code is that the data from the outer scope is still available in this function and therefore can’t be garbage collected – even if it is not needed anymore.
In this example, I’ve used localStorage. If you would like to know more about it, check out Web Storage API: localStorage and sessionStorage.
A simple way to improve the behaviour above is to add a block scope there:
1 2 3 4 5 6 7 8 9 |
{ const lotsOfData = localStorage.getItem('lotsOfData'); processData(lotsOfData); } const button = document.querySelector('#button'); button.addEventListener('click', () => { console.log('Doing something else'); }); |
Thanks to that, this data is not reachable outside the block scope and could be garbage collected after its evaluation.
If you would like to know more about the scopes themselves, check out my other article: Scopes in JavaScript. Different types of variable declarations.
Delete keyword
While in languages like C, delete keyword is actually used to free the allocated space, it is not the case with JavaScript. Since it might be a source of some misconception, let’s cover it here.
Delete operator removes a property from an object.
1 2 3 4 5 6 7 |
const dog = { name: 'Fluffy' } delete dog.name; console.log(dog.name); // undefined |
Since the delete operator has nothing to do with freeing memory directly, it will have no effect on the object that holds the property.
1 |
console.log(delete dog); // false |
Because the deletion failed, the operation returned false. On successful deletion, it will return true. It is worth noticing that attempting to delete a property that was not there, to begin with, is also considered successful in that context.
1 |
delete dog.catName; // true |
Variables declared with var in the global context are attached to the global object (in the browser it is a window). You might think for a moment, that you could delete them. There is one problem though: they are marked as non-configurable and you can’t delete properties like that:
1 2 3 4 5 6 7 8 9 |
var dog = { name: 'Fluffy' }; console.log(window.dog.name); // Fluffy console.log(delete window.dog); // false const descriptor = Object.getOwnPropertyDescriptor(window, 'dog'); console.log(descriptor.configurable); // false |
Trying to delete such property would result in SyntaxError in the strict mode.
Since arrays are just objects inheriting from the Array.prototype, you can also delete elements from the array.
1 2 3 4 |
const array = [1, 2, 3]; delete array[0]; console.log(array[0]); // undefined |
The code above indicates a problem of some sort. Deleting an element this way just creates an empty field. You might expect other elements to change their position along with that. Consider using Array.prototype.splice:
1 2 3 4 |
const array = [1, 2, 3]; array.splice(0, 1); console.log(array[0]); // 2 |
Summary
Memory management in JavaScript is something that is taken care of for you. In this article, we went through allocating and freeing the memory. If you know how this happens you can help to optimize the process. With this knowledge, you might start writing even better code!