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 aCompiler will ask Scope if there is a variable namedais 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=10and hands it over to Engine. - Now Engine will see the variable
aand 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 forfooso that we can use it. That means RHS. - Note the parameter
ain line 1. It is actually LHS. After the function callfoo(100), the line 1 is replaced asfoo(a=100). Here the value100is assigned toa(i.e.) we are giving something. That is LHS. - In line 2
console.log(a), theais RHS, since we are looking up the value ofahere. 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 variableaand pass it along to thelogfunction of the built-inconsoleobject. - When the code above is run, Compiler generates the code and requests the Scope to create a function named
fooand variable namedainsidefoo. 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 thefoois 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,
Compilerasked 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 foraonfoo. You have it ?Scope Yeah. It is declared as a parameter of
foojust now.Engine Thanks. Now let me assign
100to 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
loginsideconsole.Engine There is a RHS reference for
a. Can you help me out ?Scope Definitely. He is the same
athat was declared as the parameter tofoo.Engine Nice. Passing the value of
awhich is100tolog(...)
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
20to itEngine Is there a function named
add2in your list ?Global Scope Yes. Here you go.
Engine Excellent, let me call the function and pass the value
20to it. So, I am insideadd2now. Here after I should stop checking with the global Scope for variables. Hey Scope insideadd2function, do you know anything abouta?add2 Scope Yes, it is assigned as a parameter to the function
add2Engine Great. Let me assign the value
10to 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
add2function’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
btoo ?add2 Scope Nope. I don’t
Engine Oh ok. Then I should check with the Scope outside of
add2function’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
aandband send the result to thelogfunction of theconsoleobject.
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
sumandproductin themathFunction’s Scope. - Since those functions are available inside, the Engine executes
sum. - Inside
sumfunction it encountersaandb. And checks thesumfunction’s Scope for those variables. Now, theais 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.bis not available even there. So it checks the Scope outsidemathFunction, which is the global Scope. Finally Engine finds thatbis available in the global Scope, outside ofmathFunction, and makes use of it. This process is repeated for theproductfunction 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)
ReferenceErroris 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 namedband also noting that it is a LHS operation, will create a new variable namedbin 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); }ReferenceErrormeans Scope resolution has failed, whereasTypeErrorimplies 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
n1declared in the global Scope is available in the global Scope and in the Scope ofnumbersandproductfunctions.The variable
n2declared in the Scope ofnumbersfunction is available in thenumbersfunction as well as in the nested functionproduct. Theproductis a function nested inside thenumbersfunction.The Lexical Scope of a piece of code is all the Scopes that are available to it. For the
productfunction the lexical Scope includes the global Scope, thenumbersfunction’s Scope and its own local Scope. That means all the variables, function, references that are available in those scopes, also available in theproductfunction’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
formatterfunction returns another function namedformat. Remember, we can store functions in variables and pass them around. In this case, when calling theformatter, it returnes theformatfunction. - Now what this
formatdoes ? It simply joins the variablegreetingwith the variablename. Pay attention to thegreetingvariable. This variable is available inside the Scope of theformatterfunction. By the definition of the Lexical Scope, this variable is also available to all the nested Scope of theformatterfunction. That means only one,format. Theformatfunction has access to its local Scope, the enclosing Scope (i.e.)formatter’s Scope and finally global Scope. - This Lexical Scope of the
formatfunction always stays with it, where ever it goes. One can pass around theformatfunction anywhere in the application. It will still have access to thegreetingvariable. Why is this such a big deal ? Because we might think that once theformatterfunction 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 theformatteris still being used by theformat. This reference thatformathas over the Scope offormatteror 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
formatand 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.