This technical explanation seeks to explain JavaScript's execution context and lexical scope.
Global Execution Context
When your JavaScript code initially runs, it creates what is called a Global Execution Context.
This context gives you access to two things:
First is the global
this
The second is a global object. In the browser, this global object is the window.
In the above image, I have opened a web page that only has HTML. You can see a single paragraph element on the webpage. Yet, in the console, if you type this
and press enter, then type window
and press enter you will see that both are available. This is because currently, they are the same thing.
this === window
Global Variables
In JavaScript(JS), if you create a variable var aNewVariable = "Hello world"
the variable will become globally available. Look at the variable in the console.
Inside this JS panel, I added the new variable.
In the console, I call that variable by its name or with the global window
object. If you type in window
and open up the console you will also see the new variable.
Example code:
https://codepen.io/cgorton/pen/KKdWqQE
This is an example of JavaScript's Lexical Environment or Lexical Scope.
Lexical Environment
In the previous example, the variable is scoped
to the global window object. If you create extra functions or variables those would also be scoped
to the global object.
The lexical scope refers to where the code is written.
Here is an example of where a function would not be globally scoped.
In this example, I've created a function called myName()
that returns another function called firstName()
. If I were to go to the console and type firstName() what do you think would happen?
You will see undefined.
This function is scoped
for the myName()
function and is not available on the global object.
myName()
is available on the global window object and when we type in myName()
we can now see our firstName function and what myName
returns.
In this case, firstName
is executed inside our myName
function and returns "Christina" .
Example code:
https://codepen.io/cgorton/pen/abvJGpw
More on function execution context in a moment.
Hoisting
"Hoisting is a JavaScript mechanism where variables and function declarations are moved to the top of their scope before code execution." - Mabishi Wakio
If you have a variable declared with var
or a function
declaration, JavaScript will hoist
it or allocate memory for it after the first run-through of your code.
So if your code looked something like this:
console.log(perfectMatch)
austenCharacter();
var perfectMatch = "Willoughby"
function austenCharacter() {
console.log("Colonel Brandon")
}
What would you expect to see in the console?
In the console, you will see undefined
and Colonel Brandon
.
So, what is going on here?
When the JS engine had a first pass at the code it looked for all of the var
variables and functions and allocated memory to them.
So in the case of perfectMatch
, when the code runs the first time it stores the variable perfectMatch
as undefined. The variable is not actually definied until later on in the code, but you do store the actual variable in memory.
The function is also hoisted
or stored in memory but because it is a complete function we can execute the code inside even if austenCharacter();
is called before the function is defined in our code.
Because it has been hoisted
, JavaScript has kept this function in memory, and wherever you then place the function call austenCharacter();
, no longer matters.
Local execution context
Another type of execution context happens within functions.
When a function is called, a new execution context is created.
Below, is a common Javascript interview question surrounding local execution context.
After learning a little about scope and hoisting, what do you think will happen when this code is run?
var perfectMatch = "Willoughby"
var newMatch = function () {
console.log(perfectMatch + " is the perfect match") // what do we expect?
var perfectMatch = "Colonel Brandon"
console.log(perfectMatch + " is the perfect match") // what do we expect?
};
newMatch()
You might expect the first console.log
to be "Willoughby is the perfect match" and the second to be "Colonel Brandon is the perfect match".
What will actually appear is similar to what happened in the previous example.
First, you get undefined and then get
"Colonel Brandon is the perfect match".
Example code:
https://codepen.io/cgorton/pen/OJypOoE
When the function is called, it looks inside itself for the scoped variables.
A new execution context, in this case, a function or local execution context, is executed.
So within the function, JavaScript looks for the var
variables and then runs the console.logs
.
It allocates the variable perfectMatch
to undefined initially. So when you run the first console.log(perfectMatch + " is the perfect match")
it returns undefined
.
After, var perfectMatch = "Colonel Brandon"
is defined.
So the second console.log
returns "Colonel Brandon is the perfect match".
The code:
var newMatch = function () {
console.log(perfectMatch + " is the perfect match") // what do we expect?
var perfectMatch = "Colonel Brandon"
console.log(perfectMatch + " is the perfect match") // what do we expect?
};
A representation of the code after hoisting:
var newMatch = function () {
var perfectMatch = undefined // our hoisted variable
console.log(perfectMatch + " is the perfect match") // So now this console.log is undefined
var perfectMatch = "Colonel Brandon" // we now define our variable as "Colonel Brandon"
console.log(perfectMatch + " is the perfect match")
// Now we can console.log our newly defined variable:
// "Colonel Brandon is the perfect match"
};
Conclusion
Delving into the intricacies of JavaScript's execution context, lexical scope, and hoisting is more than an academic exercise – it is a valuable tool for mastering the language. Knowledge of these concepts can help developers write robust and efficient JavaScript code.