Learn JavaScript: Summarising You Don’t know JS (Scopes & Closures) Book 2

Abhishek
16 min readAug 26, 2023

--

Hi, I’m a frontend developer working with JS, and planning to read YDKJS by Kyle Simpson for a long time and never succeeding, this is another try, I’ll be summarising the books in these posts as I read them.

Photo by Nick Morrison on Unsplash

This is the second post, and in this one I’m sharing my learnings from the second book of the series, “Scopes & Closures”. If you are interested in the first post (about book 1, Up & Going), the post is here.

As the title of the book suggests, this book is all about scopes and closures, we get a deep dive into the compilation process, scopes, how are scopes achieved, different types of scopes, hoisting and closures and it’s implementations and uses.

Is JavaScript a compiled or an interpreted language?

Although JavaScript falls under the category of dynamic or interpreted languages, it is in fact a compiled language.

It is not compiled well in advance like most compiled languages, where the compiled version is stored and run later in time, like .c files are compiled to .exe files, which are run later.

Nor is the result of the compilation portable among various distributed systems, like in Java.

The JavaScript engine has a compiler, which generates the machine code from the human readable JS code, and the engine then runs this compiled machine code.

Few important pointers on compilation are-

  • In most of the compiled languages, the code goes through three main steps before it is executed, collectively called compilation. The steps are
    - tokenising/lexing (creating a stream of tokens from code),
    - parsing (creating AST from the stream of tokens),
    - code-generation (creating machine code from the AST)
    This is just a simplified explanation, compilation and compilers are far more complicated than this, there are various optimizations which are made for efficient execution of the code.
  • The JavaScript engine does not get the luxury of having plenty of time for optimizing like other languages, because in those languages the code is compiled at build time, like in Java, C++, unlike JavaScript, in which the code is compiled mere microseconds before it is executed.
    To ensure the fastest performance, JS engines use all kinds of tricks (like JITs, which lazy compile and even hot re-compile, etc.)

What is Scope?

One of the most fundamental feature of all programming languages is the ability to store data in variables, and access and update the data using the variable identifiers.

The well-defined set of rules for storing variables, and finding those variables at a later time are Scope. Rules for looking up variables by their identifier names accessible to the currently executing code.

The author tries to explain how the engine works end to end, from compiling to executing the code by trying to show it as a conversation between the engine and it’s friends, the compiler and scope.

var a = 2; looks like a single statement, for which we can assume that the compiler will produce the code which summed up in pseudo-code is “allocate memory for a variable, label it a, put the value 2 in it”, but that’s not how the engine sees it, the engine sees it as two separate statements, one of which the compiler will handle during compilation, and one which the engine will handle during execution.

  • The compiler encounters var a, checks with scope if a variable named a already exists for that scope collection, if it exists the statement is ignored, otherwise the compiler asks scope to declare a variable labelled a for that scope collection.
  • The compiler then encounters a = 2, and generates code be executed later by the engine, the engine will ask scope if there is a variable named a in the current scope collection, if found the value 2 is assigned to a, if not found the scope looks in the enclosing scopes, if the variable is not found anywhere, the engine then shows an error.

LHS and RHS lookup

One new thing that I learned is, there are two types of variable lookups that scope performs, LHS and RHS, the type of lookup performed depends on the operation being performed, is the variable being assigned something? or just the current value of the variable is required?

who’s the target of the assignment (LHS) and who’s the source of the assignment (RHS)

When the variable is the target of assignment, a LHS lookup is done, in which we need to find the memory address of the variable, so we can store the new value that is being assigned.

When the variable is the source of assignment, a RHS lookup is done, in which we just need to know what data is stored in the variable.

LHS and RHS result in different errors when the variable being looked for is not found

  • In case of RHS lookup if the variable is not found, it results in a ReferenceError.
  • In case of LHS lookup, if the variable is not found, a variable is created in the global scope and the assignment is completed when strict mode is disabled. A Reference Error is thrown if strict mode is enabled.
  • In case of LHS, if the lookup is successful but you try to perform an action that is not possible on the type of value that is found, it results in a TypeError.

Lexical scope in JavaScript

There are two predominant models on which scope works, lexical scope and dynamic scope.

Lexical scope is the scope that is defined at lex-time, lex-time is the time at which lexing is done, as discussed earlier, lexing is the first step of the compilation process which is responsible for tokenising a stream of characters that is your code.

Lexical scope is based on where the variables and the blocks of scopes are authored by you.

The lookups in lexical scope are hierarchical, if the variable is not found in the current enclosing scope, then it is searched in an outer scope till it reaches the global scope, the first found instance is used and no further searching is done, and if the variable is not found even in global scope then an error is thrown.

The scope is set at the time of authoring and cannot be changed later during execution, except there are some frowned upon ways to do this (change the scope set during author time later during execution).

One such way is using eval(), consider this example,

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

var b = 2;

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

By default, if a string of code that eval(..) executes, contains one or more declarations (either variables or functions), this action modifies the existing lexical scope in which the eval(..) resides. eval(..) can at runtime modify an author-time lexical scope.

there is one catch though, if eval() is executed in strict mode, it is executed in it’s own lexical scope, which means declarations made inside eval do not affect the enclosing lexical scope.

Other such ways of hampering the author-time lexical scope during execution are

  • setTimeout and setInterval, they can take strings as their first argument which is evaluated using eval
  • the new Function() function constructor takes a string of code as it’s last parameter, which is turned into a dynamically generated function.

These look like good features which provide capabilities to have dynamic code which can probably work better in certain scenarios. Then why are they frowned upon?

The JavaScript Engine performs a number of performance optimizations during the compilation phase. Some of these are majorly about, being able to statically analyze the code as it lexes, and pre-determine where all the variable and function declarations are, so that it takes less effort to resolve identifiers during execution.

But when the engine encounters eval and these other constructs discussed above, it assumes that the optimizations would render invalid because the code inside eval could modify the lexical scope.

So the engine does not perform those optimizations at all, because of this even if the code passed to eval does not change the lexical scope, the code would tend to run slower, just because of the presence of eval.

Function vs Block Scope in JavaScript

We know that scope is created by enclosing a block of code inside a function body, which creates a new scope and the variables in that function are only accessible to that function and not outside.

Common thinking about functions is that you declare a function, and write the code in it, another way to think about functions is take any section of code and enclose it inside a function.

This creates a scope for that section of code, and the variables and functions inside that block of code are now accessible only inside the block and not outside, this “hides” the variables which the outer scope does not need or is not allowed to use. Let us see this code snippet

function doSomething(a) {
b = a + doSomethingElse( a * 2 );

console.log( b * 3 );
}

function doSomethingElse(a) {
return a - 1;
}

var b;

doSomething( 2 );

this looks perfectly normal, but do you see that var b is declared in the global scope and so is doSomethingElse, but they are only used inside doSomething so their presence in the global is not necessary, a better way to structure the above code is this,

function doSomething(a) {
function doSomethingElse(a) {
return a - 1;
}

var b;

b = a + doSomethingElse( a * 2 );

console.log( b * 3 );
}

doSomething( 2 ); // 15

Now, we have doSomethingElse and b declared inside doSomething and are not exposed to the global scope, this is useful when doSomething performs some critical task, which if present in the global or outer scope may be dangerous or undesirable in any way.

Another benefit is that, after doSomething has completed executing, the memory assigned to b and doSomethingElse can be freed to be used elsewhere.

One other benefit of hiding code inside scope is, it avoids collision of variable names.

Now it may look like enclosing everything in function scope is the solution to hiding variables and functions from the outside scope, it works but it’s not the ideal solution because we declare a function which is also identified by a name, which itself pollutes the enclosing scope.

This problem could be solved if there was a way to have functions without names, or the names didn’t pollute the enclosing scope, or the function could automatically be executed.

JS provides a solution for this

Immediately Invoked Function Expression (IIFE) in JavaScript

A function expression that is immediately invoked or executed is an IIFE. For instance this code snippet, foo pollutes the enclosing scope with the function name,

var a = 2;

function foo() { // foo pollutes the enclosing scope
var a = 3;
console.log( a ); // 3
}
foo(); // have to call foo() separately in the enclosing scope

console.log( a ); // 2

The above code can be transformed to use IIFE and prevent polluting of the enclosing scope like this,

var a = 2;

(function() { // inserted "(" before function
var a = 3;
console.log( a ); // 3

})(); // enclosed with ")" and "()" in the end

console.log( a ); // 2

A function expression is a statement in which the first identifier is not function, like the above snipped the first identifier is ( and not function.

So, by adding enclosing the function expression in a ( ) pair, we execute the function immediately by adding () in the end.

Arguments can also be added to an IIFE, like this

var argToPass;
(function foo(arg1) {
...
})(argToPass);

Named and Anonymous functions

While writing a function expression it is possible to skip the function name and write it as function() { ... } this is an anonymous function.

Only function expressions can skip the name, function declarations must have a name, otherwise it would be invalid JS and will throw an error.

Function expressions are mostly seen in callback functions for instance as a callback to setTimeout and setInterval like this

setTimeout(function() {
// do something
}, 1000);

these expressions can be named functions as well, like this

setTimeout(function doSomething() {
// do something
}, 1000);

There are certain drawbacks of using anonymous function expressions,

  • As anonymous functions don’t have a name, there will be no name present in the stack trace in case of any error, making debugging difficult.
  • Without name the function cannot refer itself, for recursion, or in an event handler if it needs to unbind itself from the event after execution.
  • A name can give an idea to the developer what the block of code does, anonymous functions take away that benefit.

All these drawbacks are gone if we use named function expressions, without any downside, so it is advised to always use Named Function Expressions.

Block Scope in JavaScript

While function scope being the major unit of scope for JavaScript, many other languages support block scope.

Block scope means that any variables or function declarations present in a block, i.e. enclosed within { } pair are only accessible to that block of code and not outside. Take this code snippet for instance,

for(var i = 0; i < 10; i++) {
// do something with i
}

here we declare var i which is used inside the block of code created by the loop and not outside, we don’t need i outside of the block. But, because variables declared using var are not block scoped, we would be able to access i outside of the block, i will also pollute the enclosing scope and ideally the memory allocated to i should be cleared once the execution exits the code block because it’s not used outside the block.

JavaScript did not support block scope out of the box in earlier versions,

One new thing I learned is that, the variable declarations in a catch block in a try catch statement are block-scoped to the catch block, they cannot be accessed outside of the catch block.

In ES6 specification of the language, let was introduced, which is a way to declare variables just like var, but the variables declared using let are scoped to the block they are declared in.

the above for loop code snippet works much better with let, like this

for(let i = 0; i < 10; i++) {
// do something with i
}

now i will be scoped to the block created by the for loop.

One other feature that ES6 added was const, it is also a way to declare variables (constants more appropriately) just like let and var. const is also block scoped like let, the difference is that it creates a constant, the value assigned to a variable declared using const can not be changed in the future. An attempt to change the value results in an error.

Hoisting in JavaScript

Assuming you have read the content above, you would have a good understanding of how scope works, what according to you would be the output of the following code snippet?

a = 2;
var a;
console.log(a);

It would be 2, and the reason for that is Hoisting. One more interesting snippet is this,

console.log(a);
var a = 2;

this one logs undefined, also because of Hoisting. What is hoisting?

We discussed in the starting of this article about how the engine compiles your code before executing it, part of which was to find and associate variable and function declarations to their respective scopes.

So, in other words, the declarations in your code are processed first, during compilation, before any of the code is executed.

Which makes our code snippets work like, if it were written like this,

// snippet 1

var a;
a = 2;
console.log(a);

// snippet 2

var a;
console.log(a);
a = 2;

You see how the declarations of a are moved to the top, this is Hoisting.

Function declarations are also hoisted. But function expressions are not. For instance

foo(); // works fine because the declaration is hoisted
bar(); // throws type error because bar is undefined as
//function expression is not hoisted

function foo() {
// do something
}

var bar = function bar() {
// do something else
}

Considering hoisting, the above code can be interpreted as,

function foo() {
// do something
}
var bar;

foo();
bar(); // cannot execute undefined, so type error

bar = function bar() {
// do something else
}

Only declarations are hoisted, not assignments, because changing the order of assignments would change the way your code works completely.

Hoisting is per scope, this process is repeated for every scope in your code.

Functions are hoisted first, before variables.

Consider this snippet and analyze that functions are hoisted first.

foo(); // 1

var foo;

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

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

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

foo(); // "b"

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

Closures in JavaScript

The author points out that, “understanding closure can seem like a special nirvana that one must strive and sacrifice to attain”.

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 us see a code snippet which will make the definition clearer,

function multiplyBy(multiplier) {
// multiplier is in multiplyBy's scope

function multiply(number) {
// number is in multiply's scope
// multiplier is also accessbile in multiply's scope
// as per lexical scope rules

return number * multiplier;
}
return multiply;
}

var multiplyBy2 = multiplyBy(2); // returns a function
multiplyBy2(4) // returns 8

Here we have a multiplyBy function which takes a multiplier parameter and returns a function which takes a number parameter and returns the result of multiplier*number , multiplyBy can be used to create functions which can multiply a number by any multiplier. How does it work? and where is the closure?

The multiply function has a lexical scope access to multiplier which is in the inner scope of multiplyBy, but we are then returning the function multiply from multiplyBy, and multiplyBy finishes executing. In this case, normally we would expect that the inner scope of multiplyBy would go away, because the garbage collector used by the engine works for freeing up memory that is no longer in use, so is the inner scope of multiplyBy, not in use.

But closures does not let it happen, that inner scope of multiplyBy is still in use, it is being used by the returned function multiply.

we create a variable multiplyBy2 = multiplyBy(2) by executing multiplyBy with the multiplier parameter = 2, and it returns a function which would be stored in multiplyBy2 and upon execution, will return the result of multiplying whatever number you pass, by 2.

As you can see that when multiplyBy2 is executed, we are basically executing the multiply function, just using a different identifier, but we are executing it outside it’s declared lexical scope, which was the inner scope of multiplyBy .

The most asked loop question in interviews

What is the output of this code snippet?

for(var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, 1000);
}

// it logs "6", five times

because the function timer has a closure over i, but i is not block scoped because it is declared using var, so all executions of timer have a closure over the same instance of i, which has the value 6 at the time timer is executed.

To fix this we need the closure of i to be different per iteration of the loop, for this we need to introduce a function scoped variable in between or use let to declare i instead of var.

// solution 1
for(var i = 1; i <= 5; i++) {
(function IIFE() {
var j = i;
setTimeout(function timer() {
console.log(j);
}, 1000);
})();
}

// solution 2
for(var i = 1; i <= 5; i++) {
(function IIFE(j) {
setTimeout(function timer() {
console.log(j);
}, 1000);
})(i);
}

// solution 3
for(let i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(j);
}, 1000);
}

Modules in JavaScript using Closures

Modules are a very powerful use-case of Closures, modules are basically a code pattern which expose public features as properties of an object and keeps the private code internal and does not expose it. For instance take this code snippet

function ApiModule(endpoint) {
var apiKey = "dfgdg";
var apiEndpoint = endpoint;
var user = "";

function loginUser(user) {
// do something
}

function fetchData() {
//do something
}

/* the functions and data declared above
* are in the internal scope of ApiModule
* and are not exposed until returned from the function
*/

return {
loginUser,
fetchData,
user,
};

// only this returned data is accessible outside
}

var Module = ApiModule("endpoint");
Module.loginUser(user);
Module.fetchData();
Module.apiKey; //undefined as it's not exposed

The snippet is self explanatory, but to give more details, the function ApiModule is a normal JS function, which has some variables and functions in it’s inner scope, and the function returns an object of the data that will be exposed to the users to be used.

The ApiModule can be executed any number of times to create any number of copies of the module with different endpoints, a slight variation of the above code pattern, which allows to create only a single module instance is this

var foo = (function CoolModule() {
var something = "cool";
var another = [1, 2, 3];

function doSomething() {
console.log( something );
}

function doAnother() {
console.log( another.join( " ! " ) );
}

var publicApi = {
doSomething: doSomething,
doAnother: doAnother
};

return publicApi;
})();

foo.doSomething();
foo.doAnother();

This pattern leverages IIFE to execute the module creating function only once.

ES6 modules

ES6 introduces first-class syntax support for the concept of modules, ES6 treats each file as a separate module, each module can import other modules, as well as export their public members.

There can be only one module per file.

ES6 modules are static, that is the API that is exported can not change at runtime like it can, by changing the publicApi in our function based modules.

The compiler also checks when importing stuff from modules, if the imported stuff even exists in the module, if it does not, the compiler throws an early error.

Some pointers from the Appendix

Dynamic scope

While lexical scope is decided at author time, Dynamic scope is decided at run time and it’s not based on the order of how the code is written but how it is called, dynamic scope is based on the call stack, this in JS is kind of like dynamic scope. Lisp, LaTeX, Emacs use dynamic scope.

Polyfilling block scope

We can now use let to access block scope, but it is not available in pre-ES6 environments, how can we polyfill block scope in those environments?

catch is block scoped in ES6, so that is used by many transpilers like Babel to polyfill block scope in pre ES6, how?

try { throw 2 }
catch(a) {
// a is not block scoped
console.log(a)
}

Not very neat, but we should not use IIFE because a function wrapped around any arbitrary code changes the meaning, inside of that code, of this, return, break, and continue.

Performance wise IIFE is better than try catch, but its not the most wise polyfill to use because of the above reaons.

This wraps up the second book of the series. It was a good learning for me as always, I hope this was helpful. On to the next one. ❤

--

--

Abhishek

Frontend engineer working on Web and Mobile apps using React and Vue