The scope model that JavaScript employs is called Lexical Scope. So what is it exactly?
A scope acts like a container in which your variables and functions are declared.
var name = 'Daniel';
function printName() {
console.log(name);
};
printName(); // Daniel
There are two scopes in this example. The global scope, which encapsulates everything, and the scope of the function printName
.
Lexical Scope is a set of look-up rules that use the location where your variable is declared to determine where that variable is available. Functions have access to variables declared in their outer scope. In this case, the function printName
has access to the variable name
located in its outer scope.
One way to think about lexical scope is that inner containers always have access to variables and functions in their outer containers.
Technically, the outer scope should not have access to variables and functions contained in the inner scope. At JavaScript's inception, however, this was only possible with Functions.
Function Scope
JavaScript initially only had function-based scope (until try/catch in ES3, and now let/const binding in ES6). This meant that this was the only way to hide your variables and functions.
Why is scope-based hiding important?
One reason comes from a software design principle called the "Principle of Least Privilege". Following this principle in JavaScript would mean you keep all your variables and functions available only to your immediate scope. Essentially you are hiding them from the outer scopes.
This is also helpful so you avoid unintended collision between two different identifiers with the same name. Depending on your application, identifier names can sometimes be used multiple times in different scopes for different reasons. Not scope-hiding can result in overwriting your own variables and produce in unexpected results. This is why libraries usually do have a specific global namespace (think jQuery, $) so that their variables and functions don't collide with other libraries.
Block Scope
For some time, many languages other that JavaScript supported block scope.
for (var i=0; i<10; i++) {
console.log(i);
}
This for
loop is written this way because we only intend to use our variable i
within this scope. There's one problem with this assumption. Variables declared with var
do not have block scope; In other words, block statements do not introduce a scope. i
is actually scoping itself to the enclosing scope (the global scope in this case).
This means that it doesn't matter where we declare our variables when we use var
. They automatically belong to the outer scope.
var dataReceived = true;
if (dataReceived) {
var data = getData();
data = processData(data);
console.log(data);
}
In this example, while I only intend to use my data
variable within the scope of this if
statement, it is actually available to its outer global scope. This is pretty much a fake scope I created for visual purposes. I rely on myself not to accidentally reuse data
outside of this if
scope.
There is another problem with my example. My global scope doesn't need data
at all. If this variable was memory-heavy, now it can't be garbage collected after the if
statement finishes with it. Because of lexical scope rules, this variable will now be kept around in my application even though we do not need it anymore.
So how do we fix this issue?
let
and const
JavaScript now supports block scope thanks to ES6. Identifiers declared with let
or const
do have block scope.
These keywords attach their variable declaration to the scope of whatever block they're contained in. Looking back at our previous example we can simply swap out var
with let
(assuming environment supports ES6) to improve performance. Now our data
variable gets garbage collected after it exits the if
statement freeing up our memory!
Note: There are also other things that happen when you use let
and const
. One immediate thing to note is that while let
and const
do hoist up, you cannot access them before the actual declaration is evaluated at runtime. Swapping out var
with let
can potentially break your application if you are not careful.
Scope Closure
When we write code that relies on lexical scope, we usually write closures.
Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope.
function concatFamilyName(lastName) {
function printName(firstName) {
console.log(firstName + ' ' + lastName);
}
return printName;
};
var printFullName = concatFamilyName('Escobedo');
printFullName('Daniel'); // Daniel Escobedo
So what's happening?
The function concatFamilyName
returns a reference to its inside function printName
. The function printName
has lexical scope access to its immediate outer scope. This means the variable lastName
(provided as a parameter) is accessible inside of printName
.
Now we execute concatFamilyName
and define the lastName
argument to be my last name. Then we assign the value it returns to printFullName
, which is a reference to printName
.
Finally we call printFullName
, which is actually invoking our inner function printName
.
Did you see what happened? We invoked the inner function printName
outside of its declared lexical scope, and it still remembered the value of lastName
. So even though we exited the function concatFamilyName
, printName
still kept its scope around so that it could use it later.
This is closure. It is said that the function printName
has a closure over the scope of concatFamilyName
.
Another way to think about it is that inner functions contain and keep the scope of their parent function even if the parent function has returned.
Closures may seem like a foreign concept to many, but understanding them can really help you visualize and see them happening all the time.