JavaScript under the hood - Scoping (part 1)
This article explores the scoping mechanism in JS. Mastering scope is imperative to fully understand more advanced concepts such as closures and callback functions. This article is based on the excellent PluralSight course called “Advanced JavaScript” by Kyle Simpson
Simply put, scoping refers to a process employed by the JS compiler during the compilation phase for where to look for things.
The technical term for “things” is identifiers, and these identifiers can be anything from primitives(strings, integers, boolean, etc..) to functions. JavaScript uses lexical scoping to look for identifiers, so JS is lexically scoped. Another way to say this is that lexical scope is compile time scope. I.e., at the time that your code gets compiled, the compiler will note where the identifier is PHYSICALLY present, and the identifier’s value can only be accessed in that scope.
Well, I mentioned the word compiler a couple of times in the paragraph above, so let’s talk for a couple of minutes about how JS code is parsed. For the sake of clarity, let’s assume that the JS file is added to a web page. Every browser has a JavaScript engine that will compile the code and then execute it. This means that your javaScript code gets two pass-throughs; The first pass-through is done by the JS engine to compile the code (the compilation phase) After a few micro-seconds, the the second pass-through is done by the JS Engine to execute the code (the execution phrase).
Let’s say we have the following line:
var name = "Jack";
As a human, you’ll read the above line as one sentence, something like “var name is equal to jack”. Well, that’s not how the line is read by the JS engine, which splits that sentence into two separate sentences. The first part of the sentence (var name) is processed during compile time. In technical lingo, the compiler will add a variable declaration for an identifier called “name” in the global scope (this will be explained in greater detail below). The second part of the sentence (= “Jack”) is processed during run-time. During runtime, the JS engine will assign the value “Jack” to a variable called name.
So let’s say we have the following program:
var name = "John"; //line 1
function setName(name) {
//line 2
name = "Joe"; //line 3
function nestedFunc() {
// line 4
var name = "Lauren"; //line 5
anotherName = "jack"; //line 4
}
nestedFunc(); // line 7
console.log(name); //line 8
}
setName("Ali"); //line 9
nestedFunc(); // line 10
console.log(name); //line 11
During the compilation phase, the compiler will go through the code line by line until all identifiers have been added to their respective scopes.
To elucidate this concept easily, let’s pretend the compiler (C) is having the following conversation with the scope manger (SM), starting from the first line:
C: Hey global scope, I have a variable declaration for an identifier called name. Put that in your list of scopes.
SM: Done! I have added this in global scope.
C: Hey global scope, I have a function declaration for an identifier called setName. Put that in your list of scopes.
SM: Done! I have added setName to my list.
C: (internal monologue) I realize that this a function, so I’ll recursively descend into this function until all identifiers have been properly scoped.
C: Hey scope of setName, I have a variable declaration for an identifier called name, put that in your list of scopes. ((Did you catch this ? setName has a parameter called name, which is what the compiler is referring to! This is known as an implicitly declared variable.)).
SM: Done! I have added name to my list.
C: Hey scope of setName, I have a function declaration for an identifier called nestedFunc. Put that in your list of scopes.
SM: Done! I have added nestedFunc to my list.
C: (internal monologue) I realize that this a function, so I’ll recursively descend into this function until all identifiers have been properly scoped.
C: Hey scope of nestedFunc, I have a variable declaration for an identifier called name, put that in your list of scopes.
SM: Done! I have added name to my list.
As a reminder, here’s the setName function:
function setName(name) {
name = "Joe";
function nestedFunc() {
var name = "Lauren";
anotherName = "jack";
}
nestedFunc();
console.log(name); ???
}
JSE: Hey scope of setName, I have an identifier called name, can I get a reference for this identifier ?
SM: Yes! I have this identifier registered in my scope, here’s the reference. ((returns the reference for this variable, which is the pointer for where this variable is stored in memory.)).
JSE: Great! I’ll assign the value “Ali” to this variable.
Notice how the value “Ali” is assigned to the local variable name and NOT the global variable name, whose value is still “John”.
JSE: Hey scope of setName, I have an identifier called nestedFunc, can I get a reference for this identifier ?
SM: Yes! I have this identifier registered in my scope, here’s the reference. ((returns the reference for this variable, which is the pointer for where this variable is stored in memory.
JSE: Ahh! I see that this is a function, and the parenthesis tell me I should execute this function, so I’ll put this on the call stack to be executed!
JSE: Hey, scope of nestedFunc, I have an identifier called name, can I get a reference for this identifier?
SM: Yes! I have this identifier registered in my scope, here’s the reference. ((returns the reference for this variable, which is the pointer for where this variable is stored in memory.
JSE: Great! I’ll assign the value “Lauren” to this variable.
Notice how the value “Lauren” is assigned to the identifier name registered in the scope of nestedFunc and NOT to the identifier name registered in the scope of setName, whose value is still “Ali”. The ability of the program to have a variable in an inner scope that has the same name as a variable in the outer scope is known as shadowing. That is, the variable in the inner scope shadows the variable with the same name in the outer scope. In our example, name in the scope of nestedFunc shadows name in the scope of *setName**.
JSE: Hey scope of nestedFunc, I have an identifier called anotherName, can I get a reference for this identifier ?
SM:, Hmm, I don’t have a reference for this identifier, sorry!
JSE:: That’s alright! I’ll search for this identifier one scope upward. Hey scope of setName, I have an identifier called anotherName, can I get a reference for this identifier ?
SM:, Hmm, I don’t have a reference for this identifier, sorry!
JSE:: That’s alright! I’ll search for this identifier one scope upward. Hey global scope, I have an identifier called anotherName, can I get a reference for this identifier ?
SM:, Hmm, I don’t have a reference for this identifier, but I’ll create one for you! Here you go: ((creates and then returns the reference for this variable, which is the pointer for where this variable is stored in memory.))
Note what happened above; if the enclosing scope doesn’t have a variable registered in its lexical environment, the javaScript engine will keep searching for the variable in the outer scope until it hits the global scope. If the variable has not been registered in the global scope, the scope manager will create the variable and give back the reference! This can cause all sorts of bugs, headaches and heartaches because you inadvertently pollute the global scope. For example, look at the following block of code:
function testFunc() {
foo = "bar"; //<-- forget to declare foo in the local scope.
}
//...some non trivial amount of code later
var foo = "baz"; // <---declare foo in the global scope, thinking all is well.
testFunc();
console.log(foo); //outputs "bar". Surprise!
If you want to avoid dealing with this kind of bug, add the following line at the top of your javaScript file .
"use strict"; // <--- enables strict mode, which makes it more difficult to write bad javascript.
function testFunc() {
foo = "bar"; //<--- causes an error because the variable has not been declared
}
Another useful way to reduce the probability of bugs is using the let keyword instead of var to declare variables. What let keyword does is scope the variable declaration to the block as opposed to the the function.
Here’s a code snippet which highlights the usefulness of the let keyword.
for (var i = 0; i < 10; i++) {
//create an outer loop
//do work in the outer loop
for (var i = 0; i < 10; i++) {
//create an inner loop
//do work in the inner loop
}
console.log(i); //<-- outputs 10 after the first iteration of the outer loop is completed (because the inner loop ran 10 times ), whereas we wanted the output to be 1. The outer loop will not run again where as we wanted it to run 10 times as well.
}
If you’ve followed along so far, you should have the mental model to analyze this code from both the compile time and runtime perspective . Suffice it to say, since the for keyword is a statement (like an if, and not a function), a new scope isn’t created. During compile time the compiler registers the variable i in global scope; basically, we want shadowing to occur here but it doesn’t - this is where the let keyword shines!
for (var i = 0; i < 10; i++) {
//create an outer loop
//do work in the outer loop
for (let i = 0; i < 10; i++) {
//create an inner loop. Note the use of the let keyword
//do work in the inner loop
}
console.log(i); //<-- outputs 0 after the first iteration of the outer loop is completed, because the **i** in the inner loop is declared using the let keyword.
}
There’s other interesting properties of the let keyword, which you can read about here
To summarize what we’ve learnt so far: 1.JavaScript is lexical scoped, and variables are scoped to their functions. 2. You can scope a variable to the function block as opposed to the entire function by using the let keyword.
Next time, we’ll learn about how scoping works in more complicated scenarios, such as when using the this or call/bind/apply keywords.