“You Don’t Know JS:” My Understanding of Scopes and Closures

In the quest to understand JavaScript in as much depth as possible, reading the YDKJS book series by Kyle Simpson is a must. Here’s my understanding of the second book, “Scope & Closures,” in this amazing book series.

Image for post
Image for post
“Scope & Closure” by Kyle Simpson

The inclusion of variables in our program is one of the most basic requirements to perform tasks. But it also begets the questions:

1. Where do these variables live, or where are they stored?
2. How does our program find them when it needs them?

The set of well-defined rules that speak on the above questions and more is called scope.

Compiler Theory

Despite the fact JavaScript falls under the general category of a dynamic or interpreted language, it’s, in fact, a compiled language. For simplicity’s sake, I won’t delve into the various steps of compilation.

Any snippet of JavaScript has to be compiled before (usually right before!) it’s executed. So the JS compiler will take the program and compile it first and then be ready to execute it — usually right away.

If we take the example of , the JavaScript engine will break down into separate chunk characters, parse those chunks into what’s called an abstract syntax tree, and then turn that tree into a set of machine instructions to actually create a variable called (in the compilation phase) and later assign it the value of (in the execution phase).

Compiler Lookups

Here the author introduces the concept of left-hand side (LHS) and right-hand side (RHS) lookups. But the question is: to the side of what? To the side of an assignment operation.

The LHS lookup is done when a variable appears on the left-hand side of the assignment operation, as in the example, where the LHS lookup is trying to find the variable container itself so it can assign.

The RHS lookup is done when a variable is on the right-hand side of an assignment operation, as in . The reference to is an RHS reference because nothing is being assigned to here. Instead, we’re looking up to retrieve the value of so the value can be passed to . By contrast, in , the reference to is an LHS reference because we don’t actually care what the current value is — we simply want to find the variable as a target for the assignment operation.

If the variable can’t be found in the scope chain for an LHS lookup, a global variable will automatically be created (when in nonstrict mode), or a will be thrown (when in strict mode).

If the variable can’t be found in the scope chain for an RHS lookup, a will be thrown.

So whenever you see a in the console, you can be sure it relates to a scope failure.

Lexical Scopes

Lexical scope is a scope that is defined by the JavaScript author at the time of writing the code. (This is a nice way of thinking about scope!) It all comes down to where the author chooses to place his/her variables and functions (and blocks). This placement is what tells the JavaScript engine where it should perform its LHS and RHS lookups — in other words, the chain of scopes it needs to move up through to find a variable.

JavaScript uses the lexical scope

It gets easier and more interesting to understand and code when we approach the concept of nested scopes by looking at them from the point of view of RHS and LHS.

Consider:

function foo(a) {
console.log( a + b );
}

var b = 2;

foo( 2 ); // 4

The RHS reference for can’t be resolved inside the function , but it can be resolved in the scope surrounding it (in this case, it’s the global scope).

The simple rules for traversing nested scope

We start at the currently executing scope and look for the variable there. Then if not found, we keep going up one level — and so on. If the outermost global scope is reached, the search stops, whether it finds the variable or not.

The book gives a really great example of nested scopes as “bubbles inside of each other.”

Cheating Lexical

Before I discuss the two cheats or ways to modify the lexical scope, I must give a warning — for mainly two reasons:

  1. It’s a bad practice and is frowned upon by the wider JavaScript community
  2. It leads to poorer performance

And, therefore, I’ll mention them in not much detail. Nevertheless, it’s interesting. OK, here we go:

eval

Consider this:

function foo(str, a) {
eval( str ); // cheating!
console.log( a, b );
}

var b = 2;

foo( "var b = 3;", 1 ); // 1 3

Quite simply, you can see that the takes a string and treats the contents of the string as if it had actually been authored code at that point in the program.

In other words, you can programmatically generate code inside your authored code and run the generated code as if it had been there at author time.

with

is typically explained as a shorthand for making multiple property references against an object without repeating the object reference itself each time.

var obj = {
a: 1,
b: 2,
c: 3
};

// more "tedious" to repeat "obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;

// "easier" short-hand
with (obj) {
a = 3;
b = 4;
c = 5;
}

Hiding Stuff in Functions

Here we introduce the principle of least exposure. As the name suggests, we should expose only what is minimally necessary and, essentially, hide everything else.

Collision avoidance

Another benefit of hiding variables and functions inside a scope is to avoid unintended collisions between two different identifiers with the same name but different intended usages. Collisions often result in the unexpected overwriting of values.

For example:

function foo() {
function bar(a) {
i = 3; // changing the `i` in the enclosing scope's for-loop
console.log( a + i );
}
for (var i=0; i<10; i++) {
bar( i * 2 ); // oops, infinite loop ahead!
}
}
foo();

The assignment inside of overwrites, unexpectedly, the that was declared in at the for-loop.

In this case, it’ll result in an infinite loop, because is set to a fixed value of , and that’ll forever remain .

Function Declaration vs. Function Expression

This is fairly a very important concept I was completely oblivious to.

Simply put, if is the first thing in the statement, then it’s a function declaration. Otherwise, it’s a function expression.

Function declaration:

function foo() { 
console.log("This is a function declaration");

}

Consider this:

var a = 2;(function foo(){ // <-- insert this	var a = 3;
console.log( a ); // 3
})(); // <-- and thisconsole.log( a ); // 2

The as an expression means the identifier is found only in the scope where the indicates, not in the outer scope. Hence, the values inside the scope that encloses are not polluted by what’s in the global scope.

The following are the different ways of writing a function expressions:

var foo = function() { 
console.log("This is an anonymous function expression");

}
var x = function foo() {
console.log("This is a named function expression");

}
(function() {
console.log("This is a self-invoking function expression");

})()
(function foo() {
console.log("This is a named self-invoking function expression");
})()
setTimeout( function(){
console.log("This is an anonymous function expression as a callback parameter");
}, 1000 );
setTimeout( function(){
console.log("This is a named function expression as a callback parameter");
}, 1000 );

Immediately Invoked Function Expression (IIFE)

IIFE can be written in two different ways:

(function() {
console.log('First')
})();
(function() {
console.log('Second')
}());

Blocks As Scopes

Consider a for loop:

for (var i=0; i<10; i++) {
console.log( i );
}
console.log(i) //10

Now, we can clearly see that it makes a kind of sense that we declare the inside the for loop, but it’s not relevant when using because they’ll always belong to the enclosing scope. Then, why pollute the entire scope of a function with the variable that’s only going to be used in the for loop? The solution to that is and . Let’s take a look at them:

and create a block scope of their own, as does the catch in a try…catch statement.

The following examples will clear it further:

try…catch

try {
undefined(); // illegal operation to force an exception!
}
catch (err) {
console.log( err ); // works!
}

console.log( err ); // ReferenceError: `err` not found

As you can see, exists only in the catch clause and throws an error when you try to reference it elsewhere.

let

var foo = 1;

if (foo) {
let bar = foo * 2;
console.log( bar );// 2
}

console.log( bar ); // ReferenceError

The same code when run with :

var foo = 1;

if (foo) {
var bar = foo * 2;
console.log( bar );// 2
}

console.log( bar ); // 2

const

var foo = 1;

if (foo) {
var a = 2;
const b = 3; // block-scoped to the containing `if`

a = 3; // just fine!
b = 4; // error!
}

console.log( a ); // 3
console.log( b ); // ReferenceError!

Const also creates a block-scoped variable, but the value is fixed (constant). Any attempt to change that value at a later time results in an error.

Hoisting

This is, perhaps, the most important conclusion of everything we’ve learned above — and maybe the most interesting too.

Let's look at two scenarios:

Scenario 1:
a = 2;

var a;

console.log( a ); // 2
Scenario 2:
console.log( a ); // undefined

var a = 2;

Now the reason why the outputs might seem confusing is because of hoisting.

The JS engine works in two steps: The first is the compilation phase, and the next is the execution phase. The work of these two steps is different.

In the compilation phase, all variables and function declarations are processed.

After the compilation phase, in the execution phase, all the logic and assignments take place.

When you see , you probably think of that as one statement. But JavaScript actually thinks of it as two statements: and . The first statement, the declaration, is processed during the compilation phase. The second statement, the assignment, is left in place for the execution phase.

Note: In the above example, the in place part is very important. It’s also important to note that hoisting is per scope.

Our first snippet will then be:

var a;a = 2;

console.log( a ); // 2

And our second snippet:

var a;console.log( a ); //undefined

a = 2;

Functions first

A subtle yet important detail: Functions are hoisted first — and then variables.

Consider:

foo(); // 1var foo;function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );
};

is printed instead of . This snippet is interpreted by the engine as:

function foo() {
console.log( 1 );
}
foo(); // 1foo = function() {
console.log( 2 );
};

While multiple/duplicate declarations are effectively ignored, subsequent function declarations do override previous ones.

Consider the next two snippets:

Snippet 1:

foo(); // 3 since the second function decalaration overrides the first one.

function foo() {
console.log( 1 );
}

var foo = function() {
console.log( 2 );
};

function foo() {
console.log( 3 );
}

Snippet 2:

foo(); // "b"

var a = true;
if (a) {
function foo() { console.log( "a" ); }
}
else {
function foo() { console.log( "b" ); }
}

Function declarations that appear inside of normal blocks typically hoist to the enclosing scope, rather than being conditional, as the above code implies.

Closure

Closure is a fairly simple concept to understand if one understands lexical scope well enough. Essentially, we use closure quite often but without really understanding it.

We can look at closure as a way of calling a function or using a function out of its lexical scope or the ability of the function to access its lexical scope even when that function is called outside its lexical scope.

Consider this simple code:

function foo() {
var a = 2;

function bar() { // this function has a closure over the scope of foo
console.log( a );
}

bar();
}

foo(); // 2

To make the above code even clearer, consider this:

function foo() {
var a = 2;

function bar() {
console.log( a );
}

return bar; // we return the entire function of bar
}

var baz = foo(); // now baz has the same function definition as bar but it can acess bar's lexical scope

baz(); // guess what the output is, yes it's the same as above: 2

So to define it:

Closure is when a function is able to remember and access its lexical scope — even when that function is executing outside its lexical scope.

Modules

I was not familiar with the idea of modules as explained in this book, but they’re very interesting and helpful.

Modules are code patterns that leverage the power of closure.

Take this for an example:

function Module() {
var a= "This is a module!";

function func() {
console.log( a );
}

return {
func: func
};
}

var foo = Module();
foo.func(); // This is a module!

This is the pattern in JavaScript we call module.

To state it more simply, there are two requirements for the module pattern to be exercised:

  1. There must be an outer-enclosing function, and it must be invoked at least once (in the above example it is ).
  2. The outer-enclosing function must return back at least one inner function so this inner function has closure over the private scope and can access and/or modify that private state. (In the above example, it is .)

Done

It’s now time to delve into the third book on this and object prototypes

Happy coding!

Better Programming

Advice for programmers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store