In my previous blog, I introduced you to one of the main differences between function declaration and function expression in JavaScript. This was called hoisting and we saw that function declaration gets hoisted but function expression does not. This concept of hoisting can be understood as something getting pulled right on top in it’s respective scope. This understanding is good enough at a conceptual level but there is a lot more going on behind the scenes.
Before we embark on the details, an important concept to understand is the execution context of a variable or a function in JavaScript. When a variable is declared inside a function, the execution context for that variable is the function in which it is declared. If it is declared outside, the the execution context is the global context. This is true for functions as well.
The execution context can be either the global code, code inside a function or any eval code. A call to a function results in the formation of a new execution (functional) context.
var taxRatePercentage = 10; //global context
function calculateTax(amount){ //new execution context - functional context
var totalTax;
function deductInsuranceAmount(){ //new execution context when it is called - functional context
var insuranceAmount;
}
}
One of the important things to understand here is that the function deductInsuranceAmount can access the totalTax variable but the calculateTax function cannot access the insuranceAmount variable.
There has to be a mechanism through which the JavaScript engine keeps track of the variables, their values and functions in a particular execution context. This is done by maintaining a lexical environment.
So a couple of things are in play here with respect to a execution context:
- The lexical environment
- The scope chain – The reason why the deductInsuranceAmount function is able to access the totalTax variable defined in the outer/parent function.
Note – To keep it simple and make it easier to comprehend the lexical environment, I am going to leave out the discussion about scope chain in this blog. However the outer reference mentioned in the tables below will give you and idea about the same.
When a function is called, it is this lexical environment that maintains the parameters passed to the function , the variables declared inside the function and the function declarations for a particular execution context.
For the code below :
var taxRatePercentage = 10; //global context
function calculateTax(amount){ //new execution context
var totalTax = 0;
....
}
}
calculateTax(20000);
The lexical environment would look like this:
amount | 20000 |
totalTax | 0 |
outer | globalEnvironment |
And the global environment like this:
taxPercentage | 10 |
outer | null |
Internally, the lexical environment actually is split into 2 parts :
- The environment record which contains the totalTax, amount declarations as shown above.
- The outer link
Two phases of the execution context-
Now with this brief understanding of the lexical environment, let us turn our attention to the execution context. To reiterate, when a function is called, an execution context is created. However there are 2 phases involved with respect to the execution context. The 2 phases are :
- When the function is called
- When the function is actually executed.
These 2 phases affect the lexical environment. Let us see how.
function calculateTax(amount){ //new execution context
var totalTax = 0;
....
}
}
calculateTax(20000);
When the calculateTax is called, step 1 above, the execution context is created. Note that our function is not yet being executed. The JavaScript engine creates a lexical environment for this execution context and does the following:
amount | 20000 |
totalTax | undefined |
outer | globalEnvironment |
The parameters passed to the function are initialized with appropriate values but the totalTax variable declared inside remains ‘undefined’ during the creation phase. During phase 2(execution phase), the code is being executed and the environment record is read by the JavaScript engine and then modified as an when necessary.
amount | 20000 |
totalTax | 0 |
outer | globalEnvironment |
If we had a function expression inside this function :
function calculateTax(amount){ //new execution context - functional context
var totalTax = 0;
calculateInsuranceAmount();
var calculateInsuranceAmount = function deductInsuranceAmount(){
...
}
}
calculateTax(20000);
The corresponding lexical environment when the function calculateTax is called, during the creation phase would like this :
amount | 20000 |
totalTax | undefined |
calculateInsuranceAmount | undefined |
outer | globalEnvironment |
The code above will result in a error, it will complain that calculateInsuranceAmount is not a function. This is because the function expression is not hoisted, the variable which refers to the function is initialized to undefined during the creation phase of the execution context. During the execution phase, this environment record in the lexical context will be referred to and the error will be thrown.
But if we have a function declaration and the call like this :
sayHello();
function sayHello(){
console.log('hello');
}
When the sayHello is called, remember the 2 phases. During phase 1, the lexical environment is created but it contains a reference to the actual function in memory in case of function declaration.
sayHello | Reference to function sayHello in memory |
outer | globalEnvironment |
When the code is actually executed, sayHello is present in this lexical environment and it points to the actual function in memory.
If we were to contrast this with function expression :
sayHello();
var sayHello = function(){
console.log('hello');
}
//Throws
"error"
"TypeError: sayHello is not a function
This is because, during the creation phase, the lexical environment will look like this:
sayHello | undefined |
outer | globalEnvironment |
This behavior is due the fact that the function expression will be initialized at code execution stage.
This is what happens in case of variables (var) too :
function printToConsole(){
console.log(x);
var x = 20;
}
printToConsole();
This results in undefined because during creation phase the variable x has undefined value and during the execution phase, we try to access the value on line 1 above.The lexical environment is looked up and it prints undefined.
So in step 1 : (During context creation phase, the variable is created )
x | undefined |
outer | globalEnvironment |
Step 2 : The code starts executing, tries to access x by referring to the lexical environment and finds it as undefined above.
If we had it like this :
function printToConsole(){
var x = 20;
console.log(x);
}
printToConsole();
In Step 1: (During context creation phase, the variable is created )
x | undefined |
outer | globalEnvironment |
When the code starts executing , the interpreter executes var x = 20, it looks up at the lexical environment, sees an x there and modifies the value of x to look like the following :
x | 20 |
outer | globalEnvironment |
Now when the code comes to line 3, it can clearly see the value of 20 assigned to the variable x. This is hoisting from a behind the scenes perspective. This is what happens in case of function declaration, function expression.
Confusion between Lexical and Variable Environment
The specification uses both lexical and variable environment when it addresses the execution context. I have referred to the lexical environment in my explanations above. You can refer to the specification here to get a better understanding of the same. I would also urge you to read an excellent article by Dmitry Soshnikov to get a deeper understanding of the same.
My understanding is that the Lexical Environment was introduced for let,const declarations and for with,catch statements which creates block scope. Another link which explain the difference between the two.
Conclusion
Every function executes in a new execution context. An execution context among other things has a lexical environment which tracks the variables, the values of those variables and the functions. An execution context has 2 phases, creation phase and the execution phase. The lexical environment is created during the creation phase and then modified during execution phase. This gives us a feeling of hoisting of variables and functions declarations.
Further Reading
If you want to take your JavaScript skills to the next level, Toptal has a set of Q&A here to help you get started.