Taking A Step Back From JavaScript Hype To Understand Scope Resolution in ES6
Explaining JavaScript scope resolution in ES6.
JavaScript hype is real! The language itself, its community, and its ecosystems are growing extremely fast. Moreover, the existence of so many different sources of knowledge makes the language reflect the anarchy of the Web. In this context, one can spend years and years coding in JavaScript and building projects without really understanding key concepts of the language such as scope resolution.
Let’s check this example:
for(var i=0; i<3; i++){
setTimeout( function foo() { console.log(i) }, 100);
}
What do you think the output is?
If this is the first time you see such an example and your answer is
0
1
2
then you might want to checkout this article and understand more about JavaScript scope resolution and the new cool changes introduced by ES6.
What is Scope ?
Scope is a space in the code where a variable is relevant. We can think of variables and functions as living creatures and the scope as their habitats.
JavaScript Scope Before ES6
Prior to ES6, the smallest unit of scope a variable could have was a function. In other words, the scope of a variable could either be the nearest containing function or the global scope.
Example with var:
var x = "hello world"; // scope of x: global
function foo() {
var a = 1; // scope of a: foo
if(a > 0) {
var b = 3; // scope of b: foo
}
{
var e = 10; // scope of e: foo
}
while(a < 2) {
var d = 5; // scope of d: foo
}
}
const and let keywords and the Introduction of Block Scope
Since the introduction of const and let keywords, the smallest unit of scope a variable can have is now the block scope. In fact, const and let behave exactly the same way when it comes to scope: if the variable is declared with const or let then the smallest unit of scope the variable can live in is a block of code surrounded by curly brackets {}.
Previous example with let:
let x = "hello world"; // scope of x: global
function foo() {
let a = 1; // scope of a: foo
if(a > 0) {
let b = 3; // scope of b: if statement
}
{
let e = 10; // scope of e: surrounding block
}
while(a < 2) {
let d = 5; // scope of d: while loop
}
}
With const:
const x = "hello world"; // scope of x: global
function foo() {
const a = 1; // scope of a: foo
if(a > 0) {
const b = 3; // scope of b: if statement
}
{
const e = 10; // scope of e: surrounding block
}
while(a < 2) {
const d = 5; // scope of d: while loop
}
}
A Beautiful Example
With var
for(var i=0; i<3; i++){
setTimeout( function foo() { console.log(i) }, 100);
}
This is a for loop with a variable i iterating from 0 to 2. At every iteration setTimeout is called with a callback function that outputs the value of i after 100 milliseconds.
Although we naturally expect this output:
0
1
2
, running this code outputs strangely:
3
3
3
What just happened?
To understand what just happened, we can represent every iteration of the loop separately and mark the value of i before executing the iteration:
iteration #1
{ // i = 0
setTimeout( function foo() { console.log(i) }, 100);
}
Step 1- After 100 milliseconds, the engine has already finished executing the loop and by now i in the global scope is 3. The engine then goes ahead and executes the code inside the callback function foo.
Step 2- The engine looks if i was declared inside the function foo and it does not find it.
Step 3- The engine does a lookup to see if i was declared inside the containing block of the iteration with let or const and it does not find it.
Step 4- The engine does a lookup to see if i was declared inside the global scope and finds it to be 3.
Step 5- The output will be therefore 3
iteration #2
{ // i = 1
setTimeout( function foo() { console.log(i) }, 100);
}
The engine repeats the 5 steps explained in the previous iteration and the output is 3.
iteration #3
{ // i = 2
setTimeout( function foo() { console.log(i) }, 100);
}
The engine repeats the 5 steps explained in the previous iterations and the output is 3.
Same example with let
Now let’s see the same example with let keyword.
for(let i=0; i<3; i++){
setTimeout( function foo() { console.log(i) }, 100);
}
Executing this code results in the following output:
0
1
2
To understand why, let’s track the execution in the same way:
iteration #1
{ // i = 0
setTimeout( function foo() { console.log(i) }, 100);
}
Step 1- After 100 milliseconds, the engine has already finished executing the loop and by now i in the global scope is 3. The engine then goes ahead and executes the code inside the callback function foo.
Step 2- The engine looks if i was declared inside the function foo and it does not find it.
Step 3- The engine does a lookup to see if i was declared inside the containing block of the iteration with let or const and it finds it equal to 0.
Step 4- The output will be therefore 0.
iteration #2
{ // i = 1
setTimeout( function foo() { console.log(i) }, 100);
}
The engine repeats the 4 steps in the previous iteration and the output is 1.
iteration #3
{ // i = 2
setTimeout( function foo() { console.log(i) }, 100);
}
The engine repeats the 4 steps in the previous iterations and the output is 2.
Final Thoughts
Scope resolution is undoubtedly a concept to master when learning any new programming language. Although one can’t see it’s direct use, understanding scope resolution and the changes due to ES6 can be extremely helpful in:
- Increasing efficiency by avoiding unnecessary bugs due to incomprehension of the scope.
- Debugging code faster and know exactly where to investigate.
- Improving code quality.