Learn JavaScript: Summarising You Don’t know JS (Scopes & Closures) Book 2
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.
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 nameda
already exists for that scope collection, if it exists the statement is ignored, otherwise the compiler asks scope to declare a variable labelleda
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 nameda
in the current scope collection, if found the value2
is assigned toa
, 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 insideeval
do not affect the enclosing lexical scope.
Other such ways of hampering the author-time lexical scope during execution are
setTimeout
andsetInterval
, they can take strings as their first argument which is evaluated usingeval
- 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 atry catch
statement are block-scoped to thecatch
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. ❤