
In the world of JavaScript, understanding hoisting and function execution contexts is essential for writing clean and efficient code. In this blog post, we’ll explore how JavaScript hoists variables and functions and how function execution contexts work, using practical examples.
Hoisting Variables and Functions
JavaScript is known for its behavior of hoisting, which is the process of moving variable and function declarations to the top of their containing scope during compilation. Let’s dive into some code to see how this works:
console.log(x); // undefined
var x = 10;
console.log(x); // 10
In the above code, x
is hoisted to the top of its scope, so it is defined but has an initial value of undefined
until it's assigned the value 10
.
Function Declarations Are Hoisted
Function declarations are also hoisted to the top of their containing scope. This means you can call a function before its declaration:
getData(); // hello from get data
function getData() {
console.log('hello from get data');
}
Variables Declared with let
and const
Variables declared with let
and const
are also hoisted but have a concept called the "temporal dead zone." This means that while they are hoisted, you cannot access them before their declaration:
function modifyX() {
console.log(x); // undefined
var x = 20;
console.log("inner", x); // 20
console.log("inner", y); // ReferenceError: Cannot access 'y' before initialization
let y = 20;
}
modifyX()
In the modifyX
function, trying to access y
before its declaration results in an error, while accessing x
before its declaration returns undefined
.
Execution Contexts and the this
Keyword
JavaScript uses execution contexts to manage the scope and this
keyword inside functions. Understanding this is crucial when dealing with function calls like call()
and apply()
:
let personObj = {
name: "Ajay",
age: 20
};
function getDetails(name, age) {
return `Person name is ${name}, age is ${age}.`;
}
console.log(getDetails.call({}, personObj.name, personObj.age)); // "Person name is Ajay, age is 20."
console.log(getDetails.apply({}, [personObj.name, personObj.age])); // "Person name is Ajay, age is 20."
In the above code, we use call()
and apply()
to invoke the getDetails
function with a different this
context (an empty object {}
), allowing us to access the name
and age
properties of personObj
.
Custom Map Function for Arrays
You can even create your custom array methods, like map()
. Here's an example of creating a custom map()
function for arrays:
Array.prototype.customMap = function (callback) {
for (let i = 0; i < this.length; ++i) {
this[i] = callback(this[i], i);
}
};
const arr = [1, 3, 4];
arr.customMap((item, idx) => {
return item * item;
});
console.log(arr); // [1, 9, 16]
In this code, we add a custom customMap()
function to the Array
prototype and use it to square each element in the arr
array.
Avoiding Variable Re-declaration
Finally, it’s important to note that re-declaring a variable with let
or const
in the same scope is not allowed:
let greeting = "say Hi";
let greeting = "say Hello instead"; // SyntaxError: Identifier 'greeting' has already been declared
But following will work
let greeting = "say Hi";
//let greeting = "say Hello instead"; // SyntaxError: Identifier 'greeting' has already been declared
function test () {
let greeting = "hello";
console.log(greeting)
}
test()
console.log(greeting)
Conclusion:
Understanding hoisting, function execution contexts, and how JavaScript manages variable and function declarations is fundamental to writing clean, efficient, and error-free code. By grasping these concepts, you’ll be better equipped to navigate JavaScript’s unique behavior and produce more reliable code.