One of the main features when a developer writes JavaScript code is maybe the one most unknown by them. Maybe this happens just because no one writes code thinking directly or even knowing that the reason their code doesn't break is related to this feature.
But what is that feature?
Well... it is not a feature actually. It is a side-effect of how JavaScript is built and how it 'compile', run and executes. Let's dig in with an example.
Running the following in the browser dev tools will result in
var age = 14;
function getOlder() {
var age = 14;
age++;
};
getOlder();
console.log(`I am ${age} years old.`); // <-- ???
- It breaks (🤷)
- Print
I am 14 years old.
- Print
I am 15 years old.
The correct answer is 2
: I am 14 years old.
! But why?
Explaining the Execution
There is a lot of important information about AST (Abstract Syntax Tree) and how JS was conceived that will not be the case of study here but for the reader (check the References!), think like this:
When the Virtual Machine that runs inside your Browser (V8 in Chrome for example) executes the code it makes the naming resolution of each variable. This process of resolving the variables is required so while using a variable that is declared and defined it doesn't break your code. If the code tries to access some function or variable that is not properly defined yet it will output the famous:
Uncaught ReferenceError: yourVariable is not defined
.
Resolving Variables by Hand
If the result after naming resolution is accessible, the original code will be converted to something roughly similar to:
var global__age = 14;
function global__getOlder() {
var getOlder__age = 14;
getOlder__age++;
};
global__getOlder();
console.log(`I am ${global_age} years old.`); // --> 'I am 14 years old.'
Now it makes sense that the output is I am 14 years old.
, right? This prefix added is related to the Closure of each variable and method when the naming resolution happens. As can be observed, there are 2 Closures in this code:
global
getOlder
It can be noticed that the getOlder
Closure is inside the global
Closure but the variables inside the getOlder()
original function are only accessible inside those brackets.
So, it makes much more sense saying that the getOlder__age
variable only exists inside the global__getOlder()
function. A good example to validate is trying to log the variable from inside the function, outside of it:
var global__age = 14;
function global__getOlder() {
var getOlder__age = 14;
getOlder__age++;
};
global__getOlder();
console.log(`I am ${getOlder__age} years old.`); // --> Error!
The resulted output is Uncaught ReferenceError: getOlder__age is not defined
and the reason is that there is no variable with the naming resolved to global
Closure valid for getOlder__age
.
But what about the Scopes?
In the creation of a function, a Closure is created the same way as a Scope. All variables and functions inside that both are accessible to all child functions and not outside of it (except if they are exposed like it'll be discussed ahead).
Scope and Closure are almost equal but the second one has some 'super-powers': Variables and functions created inside the Closure and exposed will still work outside of it, even without the existence of Scope. This is a very tight line between those two concepts.
This is true even if those exposed items depends on other variables/functions inside the Closure but are not exposed.
Closures vs. Scopes
Using almost the same example as above with little changes in order to explain differences between these two concepts, the following code is a starting point
function main() {
var age = 14;
function getOlder() {
age++;
console.log(`I am ${age} years old now.`); // --> 'I am 15 years old.'
};
getOlder();
};
main();
With this example, the function getOlder()
will be called inside the main()
function and it'll print I am 15 years old now.
, correct? The variable age
is inside the main
scope and can be accessed by getOlder()
function.
Returning the getOlder()
function to the outside 'world' and executing it 3 times as the following example, what will be the result?
function main() {
var age = 14;
function getOlder() {
age++;
console.log(`I am ${age} years old now.`); // <-- ???
};
return getOlder;
};
var getOlder = main();
getOlder(); // <-- ???
getOlder(); // <-- ???
getOlder(); // <-- ???
- Nothing. The code will break.
- 3 times
I am 15 years old now.
- The value of the
age
variable will still increase from15
, to16
, and then to17
.
The correct answer is answer 3.
But why this happens?
Every time a Closure is created, all the variables and functions are stored inside its state. Even after the end of execution of the main()
function, the respective Closure state is still alive storing variables and functions!
Maybe the most awesome part of it is: the age
variable is lost inside that main()
Closure and is not accessible outside of it! If the next part of the code tries accessing that age
variable, it'll result in the already discussed Uncaught ReferenceError: age is not defined
error, as this variable doesn't exist outside the main()
function!
Wrap up
Some awesome differences between Closure and Scope concepts were discussed:
- Closures always store state about its variables and functions
- It is possible to expose some, all or none of those variables/functions by returning them at the end of the function that creates the Closure
- It is possible to even redefine some outside variables/functions inside the Closure with the same name and the Virtual Machine compiler will take care of it, avoiding errors in runtime and name collisions
Is this article useful for you? Did I miss something while explaining? Please, let me know in the comment section or send me a message!
Top comments (3)
Awesome post !
Thanks! 🚀
Your article brought me some insights I haven't had at first when learning about closures. Thanks!