The flow of code execution in JavaScript
JavaScript is a programming language that is commonly used for web development and can be run in a web browser or on a server using Node.js. In this article, we will discuss the flow of code execution in JavaScript and how it affects the way that code is written and executed.
Execution context and the call stack
In JavaScript, code is executed in an "execution context," which is a conceptual space in which code is run. When a script is executed, an execution context is created and the code is executed in that context.
The call stack is a data structure that keeps track of the execution context of a script. It is a Last-In-First-Out (LIFO) stack, meaning that the last function called is the first one to be popped off the stack and executed.
Every time a function is called, a new execution context is created and pushed onto the call stack. When the function finishes executing, its execution context is popped off the stack and control is returned to the previous execution context.
Here is an example of how the call stack works:
function first() {
second();
}
function second() {
third();
}
function third() {
console.log('Hello from third function');
}
first();
In this example, when the first()
function is called, a new execution context is created and pushed onto the call stack. Then, the second()
function is called, creating a new execution context and pushing it onto the stack. Finally, the third()
function is called, creating a new execution context and pushing it onto the stack.
The call stack now looks like this:
| third() |
| second() |
| first() |
The third()
function is the top of the stack, so it is the first to be executed. When it finishes, its execution context is popped off the stack and control is returned to the second()
function. When the second()
function finishes, its execution context is popped off the stack and control is returned to the first()
function. When the first()
function finishes, its execution context is popped off the stack and the script ends.
Synchronous and asynchronous code execution
In JavaScript, code is executed synchronously by default, meaning that it is executed in the order in which it is written. This means that the code following a function will not be executed until the function has completed.
However, JavaScript also has the ability to execute code asynchronously, using techniques such as timers, callbacks, and Promises. Asynchronous code is executed in a separate execution context and does not block the execution of the rest of the script.
Here is an example of asynchronous code using a timer:
console.log('Starting timer');
setTimeout(function() {
console.log('Timer expired');
}, 1000);
console.log('Ending timer');
In this example, the setTimeout()
function is used to execute a callback function after a specified time interval. The callback function is added to the event loop, which is a separate process from the call stack that handles asynchronous code execution.
When the script is executed, the console.log()
statements at the beginning and end of the script are executed synchronously and the output is:
Starting timer
Ending timer
However, the callback function is not executed until the specified time interval has passed. When the time interval has passed, the callback function is added to the call stack and executed. The output will then be:
Starting timer
Ending timer
Timer expired
It is important to note that asynchronous code is not necessarily executed in the order in which it is written. In the example above, the console.log('Timer expired')
statement is not executed until the time interval has passed, but it is not guaranteed to be executed immediately after the console.log('Ending timer')
statement.
Hoisting
In JavaScript, declarations (but not assignments) of variables and functions are "hoisted" to the top of the current execution context. This means that declarations can be accessed before they are actually defined in the code.
For example:
console.log(x); // undefined
var x = 5;
console.log(y); // ReferenceError: y is not defined
let y = 5;
In the first example, the declaration of the x
variable is hoisted to the top of the execution context, so it is accessible before it is assigned a value. The value of x
is undefined
until it is assigned the value 5
.
In the second example, the y
variable is declared using the let
keyword, which does not allow for hoisting. When the console.log(y)
statement is executed, a ReferenceError
is thrown because the y
variable has not been declared.
It is important to keep in mind that hoisting only affects declarations, not assignments. In the following example, the console.log(x)
statement will still throw a ReferenceError
, because the assignment of the x
variable is not hoisted:
console.log(x); // ReferenceError: x is not defined
x = 5;
Global execution context
In JavaScript, the global execution context is created when the script is first run. It is the default execution context for the script, and all code that is not inside a function is executed in the global context.
The global context has a few special features:
It has a global object, which is the window object in a web browser or the global object in Node.js.
It has a
this
keyword, which refers to the global object.Variables and functions that are not inside a function are attached to the global object and can be accessed from anywhere in the script.
Here is an example of how the global execution context works:
var x = 'Global';
function test() {
console.log(x); // 'Global'
console.log(this.x); // 'Global'
}
test();
console.log(x); // 'Global'
console.log(this.x); // 'Global'
In this example, the x
variable is declared in the global execution context and is attached to the global object. When the test()
function is called, it is executed in the global execution context, so it has access to the x
variable.
The this
keyword refers to the global object, so this.x
also refers to the x
variable.
Function execution context
When a function is called, a new execution context is created for the function. This is called the function execution context.
The function execution context has a few special features:
It has a
this
keyword, which refers to an object that is passed to the function as an argument.It has arguments, which are the values passed to the function as arguments.
It has a variable object, which is a list of all the variables and function declarations in the function.
Here is an example of how the function execution context works:
var x = 'Global';
function test(obj) {
console.log(x); // 'Global'
console.log(this.x); // 'Local'
console.log(obj.x); // 'Local'
console.log(arguments[0]); // { x: 'Local' }
}
test({ x: 'Local' });
In this example, the test()
function is called with an object as an argument. When the function is executed, a new execution context is created and the object is passed to the function as the this
keyword.
The x
variable in the function is not the same as the global x
variable. However, the function has access to the global x
variable because it is in the same execution context.
The arguments
object is an array-like object that contains the values passed to the function as arguments. In this example, the first argument is accessed using arguments[0]
.
Conclusion
Understanding the different types of execution contexts in JavaScript is important for understanding how code is executed and how variables and functions are accessed. The global execution context is the default execution context for the script, and function execution contexts are created when a function is called. By understanding these concepts, you can write code that is efficient and effective.