In this article, we'll cover a few mostly asked interview questions that will help you to crack your next JS interview.
Scope
The scope is an important concept that determines the accessibility of variables, functions, and objects. In JavaScript, there are two types of scopes
- Local Scope
- Global Scope
1. Local Scope
Variables that can be used in a specific part of the code are considered to be in a local scope. And these variables are called Local Variables. There are two types of local scope in JavaScript,
- Block Scope
- Function Scope
1.1 Block Scope
Block scope is an area within conditions (if
or switch
) or loops (for
and while
) or whenever you see {}
it is a block. In a block, you can declare variables using const
and let
keywords. These variables exist only within the corresponding block.
if (false) {
const msg = 'Hello World';
console.log(msg); // 'Hello World'
}
console.log(msg); // ReferenceError
The first console.log(msg)
works because the variable is declared inside the if
block, therefore the msg
is accessed from the scope where it is defined.
The second console.log(msg)
throws a reference error because the variable is accessed outside of its scope.
The code blocks of if
, for
, while
, and standalone also delimit a scope.
Keyword
var
is not block scoped
1.2 Function Scope
When you declare a variable in a function, that variable is only visible within the function.
function call() {
var msg = 'Make a call';
console.log(msg);
}
call(); // 'Make a call'
console.log(msg); // Error: msg is not defined
call()
function body creates a scope. The variable msg
is only accessible within the function scope and if we try to access it outside of its scope then it'll throw an error.
2. Global Scope
The global scope is the outermost scope and the variables can be accessed from any inner(Local) scope.
var msg = 'Hello World';
function readMsg() {
console.log(msg);
}
readMsg(); // 'Hello World'
Variables declared inside the global scope are named Global Variables, these variables can be accessed from any scope.
Lexical Scope
When a function is defined inside another function, the inner function can access the variables of the outer function. This operation is called Lexical scoping.
function outerFunc() {
var msg = 'Hello World';
function innerFunc() {
console.log(msg);
}
innerFunc();
}
outerFunc(); // 'Hello World'
The inner function is lexically bound to the execution context of its outer function.
Single Thread
Javascript is a synchronous language by default. This means that all the statements and functions execute one after the other in a predefined order. Javascript behaves this way because it has only a single thread of execution.
Thread in computer science is the execution of running multiple tasks or programs at the same time. Each unit capable of executing code is called a thread.
Single-Threaded
JavaScript is known to be single-threaded because of its property of having only one call stack, which some other programming languages have multiple. JavaScript functions are executed on the call stack, by LIFO (Last In First Out). For example, we have a piece of code like this:
const foo = () => {
const bar = () => {
console.trace();
}
bar();
}
foo();
And the call stack will have foo to enter into the call stack, then bar.
After bar() is done, it will be popped off from the call stack, followed by foo(). You will see an anonymous function underneath when printing out the stack trace, which is the main thread's global execution context.
This seems to be logical as JavaScript is a single-threaded language and there is only a single flow to execute all these functions. However, in the case that we are having some unpredictable or heavy tasks in the flow (for example making an API call), we do not want them to block the execution of the remaining codes (or else users might be staring at a frozen screen). This is where the asynchronous JavaScript comes in.
Call Stack
A call stack is a mechanism for an interpreter (like the JavaScript interpreter in a web browser) to keep track of its place in a script that calls multiple functions — what function is currently being run and what functions are called from within that function, etc.
- When a script calls a function, the interpreter adds it to the call stack and then starts carrying out the function.
- Any functions that are called by that function are added to the call stack further up and run where their calls are reached.
- When the current function is finished, the interpreter takes it off the stack and resumes execution where it left off in the last code listing.
- If the stack takes up more space than it had assigned to it, it results in a "stack overflow" error.
Example
function greeting() {
// [1] Some code here
sayHi();
// [2] Some code here
}
function sayHi() {
return "Hi!";
}
// Invoke the `greeting` function
greeting();
// [3] Some code here
The code above would be executed like this:
- Ignore all functions, until it reaches the greeting() function invocation.
Add the greeting() function to the call stack list.
Note: Call stack list: - greeting
Execute all lines of code inside the greeting() function.
- Get to the sayHi() function invocation.
Add the sayHi() function to the call stack list.
Note: Call stack list: - sayHi - greeting
Execute all lines of code inside the sayHi() function, until reaches its end.
- Return execution to the line that invoked sayHi() and continue executing the rest of the greeting() function.
Delete the sayHi() function from our call stack list.
Note: Call stack list: - greeting
When everything inside the greeting() function has been executed, return to its invoking line to continue executing the rest of the JS code.
- Delete the greeting() function from the call stack list.
Note: Call stack list: EMPTY
Hoisting
Hoisting is a JavaScript mechanism where variables and function declarations are moved to the top of their scope before code execution by the parser which reads the source code into an intermediate representation before the actual execution starts by the JavaScript interpreter. So, it doesn’t matter where variables or functions are declared, they will be moved to the top of their scope regardless of whether their scope is global or local.
This means that
console.log (hi);
var hi = "say hi";
Actually is interpreted to this
var hi = undefined;
console.log (hi);
hi = "say hi";
So, as we saw just now, var
variables are being hoisted to the top of their scope and are being initialized with the value of undefined
which means that we can actually assign their value before actually declaring them in the code like so:
hi = “say hi”
console.log (hi); // say hi
var hi;
Now, what about functions?
Well, if we are talking about function declarations, we can invoke them before actually declaring them like so:
sayHi(); // Hi
function sayHi() {
console.log('Hi');
};
Function expressions, on the other hand, are not hoisted, so we’ll get the following error:
sayHi(); //Output: "TypeError: sayHi is not a function
var sayHi = function() {
console.log('Hi');
};
ES6 introduced JavaScript developers to the let
and const
keywords. While let
and const
are block-scoped and not function scoped as var
it shouldn’t make a difference while discussing their hoisting behavior. We’ll start from the end, JavaScript hoists let
and const
.
Let’s first explore the let
keyword behavior.
console.log(hi); // Output: Cannot access 'hi' before initialization
let hi = 'Hi';
As we can see above, let
doesn’t allow us to use undeclared variables, hence the interpreter explicitly outputs a Reference
an error indicating that the hi variable cannot be accessed before initialization.
The same error will occur if we change the above let
to const
console.log(hi); // Output: Cannot access 'hi' before initialization
const hi = 'Hi';
So, to sum up, the JavaScript parser searches for variable declarations and functions and hoists them to the top of their scope before code execution, and assigns values to them in the memory so in case the interpreter will encounter them while executing the code he will recognize them and will be able to execute the code with their assigned values.
Variables declared with let
or const
remain uninitialized at the beginning of execution while that variable declared with var
are being initialized with a value of undefined.
Conclusion
I hoped you enjoy this article and it made some sense in how scope, call stack, and hoisting work with detailed explanations. If you liked it, you are more than welcome to make some comments below or leave a like (: