Understanding Scope and Closure in Javascript
Introduction
Scope determines the visibility of variable, functions and objects in your JavaScript code. If you forgot to declare some object and try to access it’s property, it is with the help of Scope that JavaScript knows that what you are trying to do is illegal and throws an exception. Understanding Scope is extremely important in creating error free code. Also they are building blocks in understanding advanced concepts such as Closure.
This is pretty long post with lot of concepts to absorb. I strongly encourage you to try the code samples simply in a browser console or using NodeJS.
Engine, Compiler and Scope
Most of us think that JavaScript is dynamic or interpreted language and not compiled like Java. It is wrong. JavaScript is “compiled”, but not well in advance like Java. In JavaScript there is no such phase because the compilation happens just before a piece of code is executed. Say you type var a = 10
in a browser’s console and press enter,then this code is compiled and instantly executed. There are 3 things that help run a program that we create in JavaScript,
- Engine - This is responsible for compilation as well as the execution of a JavaScript program.
- Compiler - A helper to engine. Generates the code that Engine executes later.
- Scope - Another helper of engine. This maintains a list of all the declared variables and enforces rules as to how they can be accessed by the currently executing code. Using this helper, Engine learns whether a variable is accessible in a function or not.
What happens when you type var a = 10
and press enter in your browser’s console
The statement var a = 10
may look like a single statement, but it is not. Engine is responsible for executing this statement. It sees this in two parts, one part it delegates to the Compiler and another to the Scope. There are lot of steps that I am leaving for brevity here, but the overview of what happens is given below.
- When encountering
var a
Compiler will ask Scope if there is a variable nameda
is already available. If available the Compiler moves on, otherwise it will request the Scope to create that variable. Finally it produces the code required for executinga=10
and hands it over to Engine. - Now Engine will see the variable
a
and will ask the Scope if it is available. If the Scope is not able to find the variable requested by Engine, then the Engine will throw an error, if it finds then the value 10 is assigned to that variable. In our case, it does have that variable, and hence notifies the Engine. Engine proceeds with the remaining step.
Engine and Scope - a back and forth
In an operation such as number = 100
the variable number
is Left-Hand-Side of the assignment operation and 100
is the Right-Hand-Side of the assignment operation. I am using this convention for name sake. Because what I mean by RHS is “find the value of the expression” and LHS by “find the variable in memory so that I can put some value inside of it”. In the example above number
is the LHS, 100
is the RHS. For the number
variable Engine will try to find its memory location so that it can put the value 100
in it.
var number = 100;
var factor = 2;
var result = factor * number;
For the LHS variables number
and factor
, Engine will assign the values 100
and 2
respectively. But for the result
result it has to look up the values of factor
and number
. Engine does it after checking for their availability with the Scope. Remember Scope is the tool that keeps track of all the variables.
- If you look in the memory to put some value then Engine performs LHS. If you give something to a variable then it is LHS.
- If you request some value from the memory to use, then it is RHS. If you take something from the memory then it is RHS.
Let us look at another example below
function foo(a){ // line 1
console.log(a); // line 2
} // line 3
foo(100); // line 4
- The function call
foo(100)
is an RHS reference. Here we are requesting forfoo
so that we can use it. That means RHS. - Note the parameter
a
in line 1. It is actually LHS. After the function callfoo(100)
, the line 1 is replaced asfoo(a=100)
. Here the value100
is assigned toa
(i.e.) we are giving something. That is LHS. - In line 2
console.log(a)
, thea
is RHS, since we are looking up the value ofa
here. We are taking some value from a variable. Even though there is no assignment operation, we are still trying to find the values of the variablea
and pass it along to thelog
function of the built-inconsole
object. - When the code above is run, Compiler generates the code and requests the Scope to create a function named
foo
and variable nameda
insidefoo
. Once this code is generated and variables are declared, the control is handed over to the Engine for the execution of the code and Engine dutifully performs it. Here is how conversation between Engine and Scope goes when Engine starts the execution. It starts at line 4 when thefoo
is called with the value100
.
Engine Scope there is a RHS reference for a function named
foo
, do you have it ?Scope Yup. I do. By the way,
Compiler
asked me to declare it momenets ago. Here, take the function.Engine Awesome, executing it and passing the function with the value
100
. There is a LHS reference fora
onfoo
. You have it ?Scope Yeah. It is declared as a parameter of
foo
just now.Engine Thanks. Now let me assign
100
to it.Engine Now there is a variable named
console
. Do you have it?Scope Ofcourse, it is a built-in object. Here you go.
Engine Thanks. Looking up for a function named
log
insideconsole
.Engine There is a RHS reference for
a
. Can you help me out ?Scope Definitely. He is the same
a
that was declared as the parameter tofoo
.Engine Nice. Passing the value of
a
which is100
tolog(...)
Nested scope
What we saw so far is looking up variables inside a single scope. In the example we were looking for the variable a
inside foo
. A function or a block can be nested inside another function or a block. In the same way, scope can be inside another. For every functional block, there is a separate Scope. This is very importance to remember. So if a variable is not found in the current scope, it is searched in outer scope, continuing until it is found or the outer most scope(the global scope) is reached.
Check this example
function add2(a) { // line 1
console.log( a + b );// line 2
} // line 3
var b = 20; // line 4
add2(10); // line 5
Let us see how the conversation between Engine and Scope goes. Since the execution starts outside of add2, let us assume we are in the global function. That means Engine will start requesting from the global scope.
Engine Hey global Scope, have you created a variable named
b
?Global Scope Yes.
Engine Excellent. Then let me assign the value
20
to itEngine Is there a function named
add2
in your list ?Global Scope Yes. Here you go.
Engine Excellent, let me call the function and pass the value
20
to it. So, I am insideadd2
now. Here after I should stop checking with the global Scope for variables. Hey Scope insideadd2
function, do you know anything abouta
?add2 Scope Yes, it is assigned as a parameter to the function
add2
Engine Great. Let me assign the value
10
to it. Now what about theconsole
?add2 Scope I do not know what that is.
Engine Oh ok. Then I should check with the Scope outside of
add2
function’s Scope. Which in this case is the Global Scope. Hey Global Scope, do you know anything about console ?Global Scope Yes. I have it with me. Take it.
Engine Thanks you. Now back to add2 Scope. Do you know anything about the variable
a
?add2 Scope Yes. It is available with me.
Engine Great. Do you have
b
too ?add2 Scope Nope. I don’t
Engine Oh ok. Then I should check with the Scope outside of
add2
function’s Scope. Which in this case is the Global Scope. Hey Global Scope, do you know anything about the variableb
?Global Scope Yes. I have it with me.
Engine Awesome. Now I have everything. Let me add
a
andb
and send the result to thelog
function of theconsole
object.
Another example,
var b= 10;
function mathFunction(a){
function sum(){
console.log(a + b); // 20
}
function product(){
console.log(a * b); // 200
}
sum();
product();
}
mathFunction(10);
- The code above starts with the function call
mathFunction(10)
. - When this function is executed by Engine, it checks for the existence of the functions
sum
andproduct
in themathFunction
’s Scope. - Since those functions are available inside, the Engine executes
sum
. - Inside
sum
function it encountersa
andb
. And checks thesum
function’s Scope for those variables. Now, thea
is not available insidesum
. So it checks the outer scope which ismathFunction
’s Scope. Here the Engine findsa
. It does the same forb
. It is not found insum
, so it checks insidemathFunction
.b
is not available even there. So it checks the Scope outsidemathFunction
, which is the global Scope. Finally Engine finds thatb
is available in the global Scope, outside ofmathFunction
, and makes use of it. This process is repeated for theproduct
function too.
Errors
- We have been using the terms LHS for finding the memory location of a variable in the Scope and RHS for resolving a variable’s value. There is a difference between LHS and RHS in how the variables are handled.
If RHS look up fails to find a variable(anywhere between local to global scope)
ReferenceError
is thrown by the Engine. Remember, we are trying to receive some value from a variable. If the variable itself is not available, there is no way we can receive it’s value right ? That’s why we are getting theReferenceError
.// global scope var a = 10; function foo(){ // local scope of foo // The Engine tries to resolve a and b. a is resolved successfully, // b is not // We will get "Uncaught ReferenceError: b is not defined" console.log(a*b); }
In contrast, if LHS look up of a variable fails (i.e.) no variable of the same name from the local Scope till the global Scope, a new variable is created by the global
Scope
(except in theStrict mode
). Remember, we are requesting a memory location or a container so that we can put some value inside of it. The global Scope seeing that there is no container(variable) available, will create one and give it to the Engine.In the example below, Engine will request the local Scope for the variable
b
. It won’t be available. Then it will go to the Global Scope. The Global Scope seeing that there is no variable namedb
and also noting that it is a LHS operation, will create a new variable namedb
in there and hand it over to theEngine
.// global scope var a = 10; function foo(){ // local scope of foo // A variable named b will be created in the Global scope // the value 20 will be assigned to it b = 20; console.log(a*b); }
ReferenceError
means Scope resolution has failed, whereasTypeError
implies that Scope resolution was successful, but that there was an illegal action attempted against the result like trying to assign one type of variable to another type.
Functional scope
JavaScript creates Scope inside every function. From ES6 block Scope is introduced. We will see about that later.
Where ever a declaration is made inside a function, those variables are considered to be part of the function’s scope. Each function that is declared, creates a scope for itself. So far whatever example we have seen they are all examples of Scope created inside functions.
// Global scope
function getTitle(gender){
var title;
// getTitle scope/local scope
if(gender==="MALE")
title = "Mr.";
if(gender==="FEMALE")
title = "Mrs.";
return title;
}
function formatName(name,gender){
// formatName scope/local scope
return getTitle(gender)+name;
}
// Global scope
var name = "Einstein";
var gender = "MALE";
var fullName = formatName(name, gender);
console.log(fullName); // Mr.Einstein
console.log(formatName("Curie", "FEMALE"));//Mrs.Curie
Just like how putting variables inside a function creates Scope and adds those variables to that Scope,we can do the reverse and cover a block of code with a function and create a Scope.
In the example above, say if we do not want to expose the function getTitle
in the global Scope, we could simply cover the entire code with a function. In this case, I have taken the function getTitle
out of the global Scope and have put it inside the formatName
function.
Why would we want to do that ? Because we should expose only what is necessary and hide everything else. This is a software design principle called Principle of Least Privilege
. What this principle states is that every program should access only the information and resources that are necessary for it. Here, JavaScript gives us the ability to hide variables, functions that are not needed by certain parts of the code.
// Global scope
function formatName(name, gender){
// formatName scope
function getTitle(gender){
var title;
// getTitle scope
if(gender==="MALE")
title = "Mr.";
if(gender==="FEMALE")
title = "Mrs.";
return title;
}
return getTitle(gender)+name;
}
var name = "Einstein";
var gender = "MALE";
var fullName = formatName(name, gender);
console.log(fullName); // Mr.Einstein
console.log(formatName("Curie", "FEMALE")); // Mrs.Curie
One more important aspect of hiding variables inside another Scope is collision avoidance. If two identifiers that are available in a Scope have the same name, then the identifier which is resolved first, is used.
// Global scope
var number = 10;
function getNumber(){
// getNumber's scope, local scope
var number = 5;
// Since the "number" is resolved with in the local scope itself,
// the value that is returned will be 5 always
return number;
}
console.log(number); // 10
console.log(getNumber()); // 5
Block scope
let
ES6 has introduced a new keyword named let
. Using let
is an another way to declare a variable. When let
is used inside a block ({...}
pair), it creates a Scope inside of that block for the variable it declares.
function foo(){
let number = 10;
...
}
console.log(number);// ReferenceError
Using let
you can create a temporary variable in side of a for loop variable
for(let i=0;i<10;i++){
// A new scope is created here for i
console.log(i);//0 1 2....
}
console.log(i);//ReferenceError: i is not defined
const
Another introduction in ES6 is const
variable. This works exactly like let
except, during the declaration a value must be assigned. It’s value is fixed. Any attempt to change the value results in error.
if(true){
const a = 10;
console.log(a);//10
a = 20;// TypeError: Assignment to a constant variable
}
Hoisting
What do you think would be the output of the following code snippet ?
Snippet 1
number = 100; // line 1
var number; // line 2
console.log(number); // line 3
Many would assume that it would be undefined
. Because we think that Engine executes the code line by line. In that logic, once could say that when the Engine encounters the variable number
at the 1st line, it would check the current Scope for its availability and since it is not available yet, it will keep on requesting all the enclosed Scope until the global Scope takes pity, creates the variable and returns it.
Then when line 2 is executed, using the same logic that we applied in the previous paragraph, we would think that a new variable named number
is declared in the current Scope and since a variable in the current Scope has higher precedence than the one in the outer Scope, we will get undefined
on line 3.
Sounds logical right ? Except it is not. The output is 100
.
Take a look at another piece of code,
Snippet 2
console.log(number); // line 1
var number = 10; // line 2
What would be output ? Againg 100
? Nope, this time it is undefined
indeed. What is happening here ? Recall that we saw that before executing a piece of code Engine requests the Compiler to compile the code. What does a Compiler do ? It asks the Scope to create variables that are undeclared yet. The thing is, before any of the code is executed, all the variables and functions are processed. When we see var number = 10
, we see that as a single statement. But for JavaScript(for Compiler to be precise) it is two statements. The 1st statement is the declaration var number
is processed at the compilation phase and the next statement number = 10
is left in the same place to be processed during the execution.
The Snippet 1
is processed like this,
During compilation,
var number; // line 2
during execution,
number = 100; // line 1
console.log(number); // line 3
Similarly, the Snippet 2
is processed like this
During compilation,
var number; // line 2
During execution
console.log(number); // line 1
number = 10; // line 2
It seems like declarations are processed separately or moved away to the top of the Scope. That is what happens. This is called Hoisting. Declaration comes before assignment always. To be exact, declaration is moved to the top and the assignment stays at the same place. One thing to remember is that this Hoisting process happens for every Scope. Global Scope, local Scope, I mean for every Scope. Not just variable declarations, but function declarations are also hoisted.
var num1 = 10;
var num2 = 20;
sum(num1, num2);
function sum(a,b){
console.log(a+b);
}
Can you guess how the code snippet above is processed ? During compilation
var num1;
var num2;
function sum(a,b){
console.log(a+b);
}
During execution
num1 = 10;
num2 = 20;
sum(num1,num2);
Here the function sum
is hoisted to the top of the Scope just below the variables num1
and num2
. That is why, even though we are calling the function sum
before it is even defined, we get the correct result.
Speaking of functions, I told that function declarations are also hoisted. But function expressions are not hoisted. Look at the code below.
sum(); // line 1
var sum = function add(){ // line 2
console.log("In sum"); // line 3
}
Here, this is modified as in the compile time
var sum;
In the execution time,
// A variable named sum is declared, but no value is assigned yet
sum();
// Now a function reference is assigned to the variable sum
sum = function add(){
console.log("In sum");
}
If we run the code we get TypeError: sum is not a function
. The var sum = function sum...
is a function expression. It is split into two parts, one is declaration and another is assignment. The declaration is hoisted. But the assignment is not. As expected.
The reason we get TypeError
is that the sum
is undefined
in line 1 even though it is declared. We are trying to use an undefined
as a function reference. This is illegal operation. If a variable is not available in the Scope and if we try to request it’s value, then we get ReferenceError
. But if the variable is available and if we try to perform an illegal operation such as calling a variable as if it is a function, then we would get the TypeError
.
Remember, all the declarations are hoisted to the top of the Scope (i.e.) to the top of the function where they are declared. Ofcourse, this does not apply to the declarations done using the let
or const
keywords. They stay exactly where they are declared.
Closure
We have so far seen about the Scope inside functions, the Scope inside blocks or even the global Scope. There is something that combines all these scopes. It is called Lexical Scope. Take a look this code snippet. By understanding this, we will be able to explore more about the Lexical Scope.
// global scope
let n1 = 10;
function numbers(){
// scope of the numbers function
var n2 = 20;
function product(){
// scope of the product function
return n1 * n2;
}
console.log(product());// 200
}
The variable
n1
declared in the global Scope is available in the global Scope and in the Scope ofnumbers
andproduct
functions.The variable
n2
declared in the Scope ofnumbers
function is available in thenumbers
function as well as in the nested functionproduct
. Theproduct
is a function nested inside thenumbers
function.The Lexical Scope of a piece of code is all the Scopes that are available to it. For the
product
function the lexical Scope includes the global Scope, thenumbers
function’s Scope and its own local Scope. That means all the variables, function, references that are available in those scopes, also available in theproduct
function’s Scope.
Now let us take this up a notch and take a look at this example. Check what would happen when the function process
is executed.
function formatter(){
let greeting = "Hello";
function format(name){
return greeting+" "+name;
}
return format;
}
// .......
// Some where in the application
// .......
function process(){
var formatFunction = formatter();
formatFunction("JavaScript");// Hello JavaScript
}
process();
- The
formatter
function returns another function namedformat
. Remember, we can store functions in variables and pass them around. In this case, when calling theformatter
, it returnes theformat
function. - Now what this
format
does ? It simply joins the variablegreeting
with the variablename
. Pay attention to thegreeting
variable. This variable is available inside the Scope of theformatter
function. By the definition of the Lexical Scope, this variable is also available to all the nested Scope of theformatter
function. That means only one,format
. Theformat
function has access to its local Scope, the enclosing Scope (i.e.)formatter
’s Scope and finally global Scope. - This Lexical Scope of the
format
function always stays with it, where ever it goes. One can pass around theformat
function anywhere in the application. It will still have access to thegreeting
variable. Why is this such a big deal ? Because we might think that once theformatter
function completes its execution, all the variables and functions declared inside might be garbage collected(simply removed from the memory). That is the case usually. But it does not happen now. Because the Scope inside theformatter
is still being used by theformat
. This reference thatformat
has over the Scope offormatter
or any other enclosing scopes, is called Closure. - Closure is created when an inner function is somehow made available to the Scope outside the outer function. In the example above, the inner function is
format
and the outer function isformatter
.
To drive home my point, let me present another example too. In this example, whenever you click a button with id “btnId”, an alert with the click count will be displayed. Here you are passing the clickCounter
function to the addEventListener
, a built-in function.
// Global scope
var counter = 0;
function clickCounter(){
++counter;
let message = "Your click count: "+counter;
alert(message);
}
const button = document.getElementById("btnId");
button.addEventListener("click",clickCounter);
The problem with this code is that the counter
variable is available in the global Scope
. This means that it is available through out the application. It is an extremly bad thing now, because any piece of code can change or update this variable. A for loop created with the same variable name in the application.
This will override the counter
variable’s value in the global Scope.
// Global scope
for(var counter = 0; counter<10;counter++){
console.log(counter);
}
Some where in the same application, one might have created a variable with the same name (i.e.) counter
. This is the kind of thing that introduces unpredictable behaviour and bugs. All this has happened because the counter
variable is not private and is available to the code which does not need it.
The closure comes to the rescue here. Check out the program below. The counter
variable is not accessible outside the clickCounter
. But it still gets updated whenever the button
is clicked. It is because the alertMessage
function has closure over the Scope of the clickCounter
.
function clickCounter(){
let counter = 0;
return function alertMessage(){
++counter;
let message = "Your click count: "+counter;
alert(message);
}
}
const button = document.getElementById("btnId");
button.addEventListener("click",clickCounter);
Closure provides data hiding here. We are not exposing the variable counter
to the outside world, but still it is doing the counting perfectly. Closure introduces the concept of private variables to the JavaScript.
Let us see another example. Now we want access to a variable. But should not be able to change it. How to implement that ? Take a look at the keyProvider
function. keyProvider
returns a function, which gives the secretKey
. This secretKey
can be viewed, but can not be changed.
function keyProvider(){
const secretKey = "abc";
return function (){
return secretKey;
}
}
var getKey = keyProvider();
var key = getKey();
console.log(key);//abc
With this I conclude my summary of Scope and Closure in JavaScript.
To know more about Scope check out this question in the stackoverflow. To know more about closure checkout this question in stackoverflow.