Lexical Scope
In part 1 of this series, we mentioned that the Compiler takes an abstract syntax tree and turns it into our executable code (parsing & code-generation). But before he does these things, he starts with tokenizing (or lexing) every line of our code. Lexing, is the process of taking all the characters of our code and turning them into pieces, giving them semantic meaning that Javascript can understand. These pieces are called tokens. Now let’s define the Lexical Scope.
Lexical scope is the scope that is defined when the Compiler passes through the phase of lexing. This means that, actually, lexical scope is defined based on where we place our variables in the code. In other words, lexical scope answers one question: “Where do our variables live”?
The answer, is that, our variables live in the particular scope we defined them in our code. But this answer brings up another question: “Where exactly can I access my variables from”? Let’s find the answer by examining the following snippet.
const a = 5;
function printInConsole(b) {
let c;
c = 2 * a;
console.log(b+c);
};
console.log(c);
printInConsole(3);
What we will see in our console?
Instead of seeing 10 and 13, we get a ReferenceError: c is not defined message. So, what went wrong here? Now, we ask lexical scope to answer the question we made before.
Variables live inside the scope they have being declared at. So, the variables b and c live only inside the function’s scope, while the variable a lives in the global scope. Thus, we see that variables that are being declared in the global scope, can be accessed by any place of our code, while, variables that are being declared in nested scopes, can only be accessed by any place inside the nested scope. Therefore, when we tried to access the variable c in the global scope in order to log it in the console, the Engine couldn’t find it, because c only lives inside the function’s scope.
Shadowing
Let’s now review another snippet.
const a = 5;
function printInConsole(b) {
const c = 2 * a;
function anotherConsoleLog(b) {
const c = 2 * b;
console.log(b,c);
};
anotherConsoleLog(3);
};
printInConsole(3);
What you think will be the value of the variable c that will be logged in the console?
To answer this question, let’s try to see how the Javascript Engine will execute this code. From which scope will it start? The Engine will start the execution from the console.log statement, thus, from the anotherConsoleLog’s scope. Then it will proceed to find where the two variables, b and c are located, so the Engine can get the values they point to. Next, the Engine will proceed to the printInConsole’s scope only to see again a variable named c. But the Engine has already found the variable c it wanted, so it will ignore the second one. The same procedure takes place in the global scope, regarding the variable a. Therefore, what we get in the console is the result 3 and 6.
In fewer, less boggling words, when the Engine finds the first match of a variable in the code, it stops looking for this particular one. This brings us to the concept of Shadowing, which states that the same identifier name can be used at multiple nested scopes, since the most nested one will “shadow” the outer ones.