Taking A Step Back From JavaScript Hype To Understand Scope Resolution in ES6

Written by seif_ghezala | Published 2017/06/11
Tech Story Tags: javascript | es6 | programming | software-development | ecmascript-6

TLDRvia the TL;DR App

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

012

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: globalfunction foo() {var a = 1; // scope of a: fooif(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: globalfunction foo() {let a = 1; // scope of a: fooif(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: globalfunction foo() {const a = 1; // scope of a: fooif(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:

012

, running this code outputs strangely:

333

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:

012

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.

Published by HackerNoon on 2017/06/11