Using Closures To Capture and Modify Primitive Variables in JavaScript
TLDR: For information on Closures and how they can share primitive arguments/ state across functions, skip to the second section. I discuss the use case in the earlier paragraphs.
One pattern I’ve encountered working with JavaScript is the need to share state between functions. If you starting out a new code base, then preferrably you would want to use classes and class properties which makes this really easy.
But in other cases, you might be working some old code. Really old code. Or it could be a project context where class-based syntax isn’t the default for some reason. How would you solve this problem then?
Before going further, it’s worth mentioning that mutating global state through functions is something you with caution. It can be the source of strange bugs. And the general best practice in JavaScript programming to make your code immutable, which means that functions do not modify original values, but instead create copies with new values. Doing so makes it easy to reason about changes. Frameworks such as React strongly emphasise immutability as a pattern, as Flavio Copes explains here - though the voices insisting that one MUST do immutable code seem to be less vocal than a few years back.
With all that out of the way, let’s say you really want to modify function arguments. What options do you have?
When using a primitive-type variable (e.g. string, number) in a function argument, the new function has no way to modify the original variables.
That’s quite a mouthful, so let’s illustrate this with a simple example:
let count = 0;
function increaseOne(numberArg) {
numberArg = numberArg + 1;
}
increaseOne(count);
console.log("Count :", count); // 0, not 1; count is unchanged
If this seems obvious to you: congratulations, you probably have decent experience in JavaScript. To those who are newer to the language, it might seem counter intuitive, especially when we look at similar code that produces wildly different results.
let countObject = { count: 0 };
function increaseOne(numberObjectArg) {
numberObjectArg.count = numberObjectArg.count + 1;
}
increaseOne(countObject);
console.log("Count :", countObject); // { count: 1 }, count has increased
Since the focus here is to explain the solution of how you to modify an external variable rather than why exactly this happens, I will lay out a general principle that you can reason with:
Pimitive variables cannot be mutated when passed to a function, but object variables allow their properties to be mutated when passed to a function.
If you wish to dig deeper into the underlying mechanism and thoroughly understand the JavaScript mechanism for passing function arguments, that is known as “call-by-sharing”, I encourage you to read more on Dmitry Soshnikov’s explantion here.
Use Closures
What are closures? There are many long explanations, including this one from MDN, but I will try to explain it as simply as possibly.
Closures are a fancy language feature. They allow lexically-nested functions to access and modify variables declared in the outer parent function (i.e. their lexical scope).
That’s still a mouthful, so let’s break it down part-by-part.
- Lexically-nested functions are function defined inside of other functions. By this I mean they are written inside the curly braces that make up the block statement of the outer function.
function myOuterFunction() {
console.log("Hello from the outer");
function myNestedFunction() {
console.log("Hello from the nested");
}
myNestedFunction();
console.log("Back in the outer function");
}
// Executing this...
myOuterFunction();
// We get:
// Hello from the outer
// Hello from the nested
// Back in the outer function
- Lexically-nested functions can access and modify variables declared in their parent function
function myOuterFunction() {
let count = 1;
console.log("Outer function: Initial count : ", count);
function myNestedFunction() {
console.log("Inside nested function, initial count : ", count);
count += 1;
console.log("Inside nested function, modified count : ", count);
}
myNestedFunction();
console.log("Outer function, final count :", count);
}
myOuterFunction();
// Results:
// Outer function: Initial count : 1
// Inside nested function, initial count : 1
// Inside nested function, modified count : 2
// Outer function, final count : 2
Wow, what just happened there?! The inner myNestedFunction does not have its own count
variable. Instead, it forms a closure on the variable count
declared in the outer function. It can read the count. It can even modify the count.
What actually happens under the hood is that closures save a memory reference to the variable name. This is key. It’s not a copy of the variable’s value, but an actual reference to that variable. This is like a magic link to the memory location where the variable is saved, and gets resolved whenever code in the inner function uses it.
Closures can wreck havoc on your code when they are unintended, as in the famous “value of i after setTimeout” interview question, see here and here
In our case, we can exploit them as a force for good because they allow us to hunt down the original variable, and even mutate it.
Hope you found this useful. Please feel free to reach out to me if you’d like to discuss more. Happy Coding, and till next time!