YDKJS — Scope and Closures
--
This blog post series will aim to summarise the most important bits and pieces from the great You Don’t Know JS series by Kyle Simpson. The remarks and examples are all his and I’m mainly summarising them here for learning purposes — if it helps you to learn too and understand JavaScript a bit better, awesome!
I’ve already hinted at the fact that we will explore another big JavaScript topic in this blog — Scope !
Some of you might have an artificial idea what scope is, but Kyle Simpson defines it as “a well defined set of rules for storing variables in some locations and for finding those variables at a later time.”
To understand Scope properly, we’ll go deep down now and talk about how JavaScript is compiled by the JavaScript Engine.
Three steps of Compilation
- Tokenizing/Lexing
- Parsing
- Code Generation
Tokenizing
- breaking up expressions or statements into chunks called tokens
Parsing
- Building a tree of nested tokens which present the grammatical structure of the program — we call this tree the abstract syntax tree (AST)
Code Generation
- This step consists of actually turning the AST into executable code
Any JavaScript snippet is compiled before the code is executed. But compiling JavaScript code is not as straightforward as you might think. The compiler will check if a variable already exists in current scope and if it does, it will keep on compiling. If it doesn’t, it will create the variable and its associated scope, the JavaScript engine will look up these variables in the scope and assign to them if they’re found.
Let’s talk about looks — or more precisely lookups!
The JavaScript Engine can make two different lookups, that we can differentiate into two groups, which refer to the side of the assignment operation.
- RHS lookup (Right hand side lookup): assigning to a variable
- LHS lookup (Left hand side lookup): retrieve the value of a variable
An RHS happens, when variable appears on — you guessed it the right hand side, whereas LHS lookup happens, when the variable appears on the left hand side. Think of it as an LHS lookup being what the variable is being assigned to, whereas a RHS lookup is going to retrieve the value you stored in it.
Nested Scope
Of course, we don’t just work with a single scope — usually we have nested scopes to consider. If you can’t find a variable in its immediate scope (the innermost scope), you proceed to check further and further out by examining the more outer scopes, until you might reach the global scope and find it. This is important and will be a really powerful concept later on, so remember it! At each stage in this chain of inner and outer scopes, you resolve the RHS and LHS lookups until you find what you’re looking for.
If an RHS fails to find a variable, we get a ReferenceError thrown due to a scope resolution failure. LHS scope will create a global variable if not found, unless running in strict mode. The other error commonly encountered is a TypeError which means that we found the scope but were trying to perform an illegal operation.
Lexical Scope
Scope can work in two different ways — lexical scope and dynamic scope. JavaScript uses Lexical Scope, which is why we will focus on it here.
Lexical scope is scope that is defined at lexing time. This means we have control to author our own scope at write time.
Let’s take the following example:
function foo(a) {
var b = a*2; function bar(c) {
console.log(a,b, c);
} bar(b*3);}foo(2); // will log 2, 4, 12
Our innermost Scope (let’s call it Scope 1) only includes one identifier c. So we keep looking in the more outer scopes whether we can find declarations for a or b. Scope 2 (the more outer scope following the innermost scope) has a, bar and b declared. The most outer scope only has one identifier, namely foo. Assume for now, that each function creates a new Scope. Scope lookups stop when they find the first match, starting at the most nested and going out.
Lexical scope is only defined by where the function was declared — not where it was called. This is important to remember and we will come back to it.
You can cheat lexical scope but it is generally frowned upon. You might have come across eval() or with() in the past, but since they also go hand in hand with big performance losses due to the JavaScript Engine not being able to perform compile-time optimisations, you should avoid using them.
Functions vs Block Scope
We just discussed function scope and the fact that each function declares its own scope bubble. They go further and encourage the idea that all the variables declared in the function belong to the function. This also abstracts the code in the function away and hides it from other code that might be globally accessible.
This idea of “hiding” the complexity is not new. In Software Design, it is called the principle of Least Privilege or Least Authority — meaning that you should only expose what’s minimally necessary.
Global namespaces
Libraries often use an object as a namespace and make its exposure possible through the use of its properties. This is why libraries tend to use unique names, so the objects don’t collide with other objects in the global scope.
Module Management
The module approach is the other and more frequently used technique today. Identifiers have to be explicitly imported into another specific scope through the use of a dependency manager.
Functions as scopes
Declaring everything as a function, might easily pollute our environment though and there are a few techniques you can use to avoid the explicit need to define an explicit named function and having to call it.
One way would be to immediately call the function and wrap it in parantheses
var a = 2;(function foo() {
var a = 3;
console.log(a); // will log 3
})();console.log(a); // will log 2
The parentheses at the start just before the word function, means that this is treated as a function expression rather than a function declaration. Moreover, it is being immediately invoked by the pair of parentheses at the very end. This way foo is only found in its enclosing scope and doesn’t pollute the enclosing scope.
Anonymous functions
Many people also use anonymous function expressions as a quick and easy way to immediately call a function. However, there is no downside to actually giving your function a name, which might make it easier for a human to read and makes it easier for debugging. It can still be used in exactly the same fashion and therefore doesn’t introduce any side effects.
Immediately Invoked Function Expressions (IIFE)
This is the term that has been given to the mechanism we explained earlier — executing your function immediately by adding a set of parentheses and therefore not polluting enclosing scope.
You might see either of these variations to achieve this, however, they both achieve the same result.
(function(){…})(); // only the first bit before the set of round parentheses(function(){…}()); // the complete code is wrapped in round parentheses
IIFEs can also be called with arguments, which makes them an even better candidate to use.
(function(global){…})(window);
Blocks as Scope
On the surface, the JavaScript language has no support for block scoping as it might be known from other programming languages.
Examples though that are quite closely related to the idea of block scope are the common for loop and an if statement, that both mirror similar behaviour.
try/catch
Catch actually defines a block scope, the error only exists in the catch clause and won’t pollute the enclosing scope.
let
The let keyword has been introduced in ES6. The let keyword attaches the variable declaration to the scope of whatever block (usually a pair of {…}). Declarations made with let will not hoist to the entire scope of the block they appear.
You can also explicitly define block scopes in your code.
let loops
for(let i=0; i<10; i++) {
console.log(i);
}console.log(i); // ReferenceError
let binds the i to the for loop body and rebinds it to each iteration of the loop, making sure to reassign it the value from the end of the previous loop iteration. The let allows declarations of variables in any arbitrary block of code and prevent it from bubbling up in scope.
const
Const will also declare a block-scoped variable, however this value is fixed and can’t be reassigned later.
Hoisting
The JavaScript Engine compiles the code before it interprets it. All declarations, both variables and functions, are looked at beforehand.
This is also the case for implicit declarations.
Take the following code snippets:
a = 2;
var a;console.log(a);// The declaration of var a is "hoisted" to the top and handled first, meaning we get 2 as output
This example hoisted the variable declaration to the top and then proceeded to execute the rest of the assignments. As a is being assigned to 2 straight after, we will get 2 for our output. Conversely:
console.log(a);a = 2;// This will not give us 2 as in the previous example but print undefined
Why does the second snippet return undefined? Through passing an undeclared and unassigned parameter, the Engine will will process this and implicit declaration which leads to something along the lines of this:
var a;
console.log(a); // it's clear now why we get undefined// the assignment of 2 only comes later
a = 2;
This process of moving declarations to the top is called “Hoisting”. We only hoist variable or function declarations but NOT assignments — this would leave too much room for error and lead to quite unexpected results.
Another distinction which is necessary to make, is that hoisting happens per Scope. We don’t hoist to the global scope but always to the enclosing scope.
You might have noticed, that I placed emphasis on function declarations, however function expressions are not hoisted. Why is that?
Take the following example:
foo(); // not Reference Error but Type Errorvar foo = function bar() {
// some code
};
While foo is hoisted and put into the global scope, it has no value yet (as it would have had if it were a declaration). Thus it fails, as it can’t invoke undefined. We only declare var foo — we don’t assign it any value yet. In the case of a function declaration, we’d move the whole function up in scope that it would be able to execute.
Another important note: functions are hoisted first, before variables. This can lead to unexpected results when you declare variables and functions with the same name. Other function declarations will overwrite each other, whereas function declarations are hoisted to the top. Moreover, functions inside a block also hoist to enclosing scope, which may cause confusion. So it’s best to avoid declaring functions in blocks.
Scope Closure
Kyle Simpon opens the last chapter of his great book with the following words:
“Closure is all around you in JavaScript, you just have to recognise and embrace it.”
Closures happen as a result of writing code that relies on lexical scope. Kyle Simpson defines it as: Closure is when a function is able to remember and access its lexical scope even when that function is executing outside its lexical scope.
Let’s look at an example:
function foo() {
var a = 2; function bar() {
console.log(a);
}return bar;
}var baz = foo();baz(); // will log 2 --> and executed outside of its declared lexical scope
So how did we cheat here? How could we access and log 2? The secret lies in passing bar as a value at the end of foo(). Doing this allowed us to return the contents of bar as function object. Since foo() is returning these contents, we’re assigning them to the variable baz which in turn holds the value of the execution of the bar function. But wait there’s more magic going on here too:
Because the function bar() has lexical scope over the contents of foo(), which keeps the scope alive for the function bar() to use. The reference bar still has over foo is what we call Closure!
Passing functions around as first class values will result in Closure and from now on you will probably find many cases where you can observe closure.
Some popular examples include:
- setTimeout (which has closure over the enclosing function and can therefore access any variables declared in it)
IIFEs however, are not in a strict sense, creating closure, although it creates its own scope.
This can come in handy as in the following example:
for(var i=1; i<=5; i++) {
(function() {
var j = i;
setTimeout(function timer() {
console.log(j);
}, j*1000);
})();
}
If we simply wanted to use the setTimeout() without the IIFE and had it try to access i, we would fail! i would instead refer to one global i, in this case with the value 6 and thus not work as intended. By creating an own scope within the IIFE and reassigning the current value of i to its own new variable j, we can make this work as intended. Et Voila! The function callbacks can now close over a new scope for each iteration.
We also learned about let previously.
let lets us hijack a block and declare it right there in the block and turns the block into a scope that we can close over. This simplifies our code to the following:
for(var i=1; i<=5; i++) {
let j = i;
setTimeout(function timer() {
console.log(j);
}, j*1000);
}
This way, we eliminated the need for using an IIFE which created its own scope by simply using a let declaration instead of an IIFE! But we can simplify this even further by using let in our for loop declaration, letting the code ultimately become:
for(let i=1; i<=5; i++) {
setTimeout(function timer() {
console.log(i);
}, j*1000);
}
This means that the variable will be declared not just for one loop but for each iteration, meaning that we have not only simplified our code but made it much more readable too.
Modules
The module is another code construct that lets us see Closure in action in the form of the module pattern.
The module pattern has two key characteristics that need to be fulfilled:
- There must be an outer enclosing function and it must be invoked at least once (each time creates a new module instance).
- The enclosing function must return back at least one inner function, so that this inner function has closure over the private scope and can access or modify the state
But that’s a bit theoretical on its own, let’s look at a code example:
function CoolModule() {
var something = "cool beans";
var another = [1, 2, 3]; function doSomething() {
console.log(something);
} function doAnother() {
console.log(another.join( " ! "));
} return {
doSomething: doSomething,
doAnother: doAnother
};
}var foo = coolModule();foo.doSomething(); // will log "cool beans"
foo.doAnother(); // will log "1 ! 2 ! 3"
There is a lot going on here, so let’s dissect here bit by bit.
In order to actually invoke any closure, we must invoke the function CoolModule() and assign it to a variable. You can see that CoolModule() returns an object that has references to both of our inner functions, doSomething() and doAnother(). This object thus returns a reference to our functions. However, it hides the inner variable and implementation details, meaning that one could say it acts an public API for the module.
The object return value is then assigned to the outer variable, which means that we can access the property by calling foo.doSomething().
There are many other cases, where the module pattern can be observed and where we can use it:
- Singleton → turns Module Function into an IIFE
- You can also pass in parameters to your Module and use them within
- Naming the object you’re about to return
Let’s look at the latter in another code example:
var foo = (function CoolModule(id) { function change() {
// modifying the public API
publicAPI.identify = identify2();
} function identify1() {
console.log(id);
} function identify2() {
console.log(id.toUpperCase());
} var publicAPI = {
change: change,
identify: identify1
}; return publicAPI;})("foo module");
// here we invoke the Module directly and pass it a parameter "foo module"foo.identify(); // will log foo module
foo.change();
foo.identify(); // will log FOO MODULE (change modified the public API to be identify 2)
The neat thing about this is that by keeping an inner reference to the public API inside your module instance, that you can modify, add and delete from it!
Modern Module managers do exactly the same thing and just fulfill the two key characteristics we defined at the start:
- There must be an outer enclosing function and it must be invoked at least once (each time creates a new module instance)
- The enclosing function must return back at least one inner function, so that this inner function has closure over the private scope and can access or modify the state
ES6 has added first class syntax support for the concept of modules, making it even easier to use them. We treat each file as a separate module in this case. There’s two important distinctions to make for using them
- import → import only imports one or ore members from the module’s API
- module → imports an entire module API to a bound variable
Closure is a very beneficial concept that lets us use patterns like the Module Pattern to clean up our code and encapsulating information. Closure can be incredibly powerful if used correctly and ensure you can access other scopes when you need to. The concept of Least Privilege or Least Authority helps you to keep things readable and simple and make your code easy to read and use.
Some closing (pun intended!) remarks
While this was only the second book in the YDKJS series, I felt like I’ve learned a ton and really digged deeper into the inner workings of JavaScript. I’ve finally understood the concept of Closure, which was a bit of a mystery to me prior to reading this book. So thank you Kyle Simpson for enlightening me and taking me on to the journey to finally master JS!
Resources
- You Don’t Know JS — Scope & Closures by Kyle Simpson, available for free to read on Github
If you enjoyed reading this summary blog posts, leave some claps and check out some of my previous posts: