doganozturk.dev

JavaScript Basics: Hoisting

almost 4 years

* This article is also available in Turkish.

During job interviews with developers on Zingat, one of the problems we often encounter is that the candidate's relationships with the fundamental elements of the language they use could be more connected. Most are interested in current web frameworks (React, Vue, etc.) or JS-based cross-platform development environments (React-Native, Ionic, etc.). Still, they need to be closer to the story of JavaScript, the realities of the language, and the environments in which it works.

I have decided to start a new series of articles to produce a resource, collect and organize my knowledge on this topic, and create a personal reference point. I am embarking on this journey to present the information I have learned from the sources I have read and watched in a structured way.

The things I will talk about here are already available on the Internet, and many things are said in Turkish and English on these topics. Naturally, those who have been developing with JavaScript for a long time will already know what is written here. However, this series of articles may be a good starting point for those who are just starting to learn the language or can use it to develop work but want to delve deeper.

When the JavaScript engine runs a piece of code, it goes through some stages. We can describe these as:

  1. Creation phase
  2. Execution phase

The JavaScript engine gives us the global object and the this keyword during the creation phase. For the browser environment, the global is the window object. this is a separate topic; we will examine it later. These are what we have as a result of the creation of the global execution context. Try giving a blank .js file to your browser and printing the global (window) and this expressions in the developer console. You will see that you can access them even though your file is empty.

One more thing happens during the creation phase. Hoisting, the subject of this article, occurs at this point. Let's have a piece of code like this:

console.log(person);
console.log(greetPerson);

var person = "ahmet";

function greetPerson() {
  console.log("Hello " + person);
}

In JavaScript, unlike many other programming languages, calling the functions that allow the person and greetPerson variables to be written to the console at the beginning of the code does not cause any errors. As a result of Hoisting, variable declarations and function declarations are hoisted. We can assume that the following situation occurs to facilitate our understanding:

var person = undefined;

function greetPerson() {
  console.log("Hello " + person);
}

console.log(person); // undefined
console.log(greetPerson); // function greetPerson()

person = "ahmet";

However, this is different. The JavaScript engine goes through all the code blocks during the creation phase. It records the variable and function declarations in the memory of the global execution context and any function execution context (each function in JavaScript has its execution context).

So why has such a need arisen? I want to share the following sentence attributed to Brendan Eich, which I also came across while preparing this article:

"var hoisting was thus [an] unintended consequence of function hoisting, no block scope, [and] JS as a 1995 rush job."

It is the result of JavaScript's unique design again.

There are some points to consider in our efforts to understand the topic of Hoisting. Let's examine them through examples:

console.log(person);
console.log(greetPerson);

var person = "ahmet";

(function greetPerson() {
  console.log("Hello " + person);
});

When we run this code, the things we explained above about the person variable are still valid; however, the greetPerson function declaration is now an IIFE (Immediately Invoked Function Expression). In this case, there is no declaration. Only the person variable will be hoisted when the JavaScript engine goes through this code.

var person = undefined;

console.log(person); // undefined
console.log(greetPerson); // ReferenceError: greetPerson is not defined

person = "ahmet";

(function greetPerson() {
  console.log("Hello " + person);
});

Now let's consider only the variable initialization situation:

console.log(person);

person = "ahmet";

We will get an error when we run the code. As we said at the beginning, only declarations are hoisted, and there is no declaration here.

console.log(person); // ReferenceError: person is not defined

person = "ahmet";

A similar situation also occurs with function expressions:

console.log(person);
console.log(greetPerson);

var person = "ahmet";

var greetPerson = function () {
  console.log("Hello " + person);
};

This time, the output will be as follows; in this context, this function expression is a variable declaration. As required by the working principle of Hoisting, it is treated in this context.

var person = undefined;
var greetPerson = undefined;

console.log(person); // undefined
console.log(greetPerson); // undefined

person = "ahmet";

greetPerson = function () {
  console.log("Hello " + person);
};

Let's develop our example a little further and include function execution contexts in it:

console.log(person);
console.log(greetPerson);
console.log(greetPerson());

var person = "ahmet";

function greetPerson() {
  console.log("Hello " + person);

  var person = "mehmet";
}

Another thing to note is that hoisting occurs in the greetPerson function, just as it does in the global execution context, and when the JavaScript engine sees the line var person = 'mehmet'; it searches for memory for the person variable (memory allocation). I would also like to draw your attention to the behavior of the person variable due to how the scope chain works in JavaScript.

var person = undefined;

function greetPerson() {
  var person = undefined;

  console.log("Hello " + person);

  person = "mehmet";
}

console.log(person); // undefined
console.log(greetPerson); // function greetPerson()
console.log(greetPerson()); // 'Hello undefined'

person = "ahmet";

Another example:

console.log(person);

var person = "ahmet";
var person = "mehmet";

When Hoisting occurs, we can assume that it will be as follows:

var person = undefined;

console.log(person); // undefined

person = "ahmet";
person = "mehmet";

So far, the examples we've seen are similar for variable declarations, so what about function declarations?

console.log(greetPerson());

function greetPerson() {
  console.log("Hello");
}

function greetPerson() {
  console.log("Hi");
}

As soon as the JavaScript engine sees the first greetPerson declaration, it will hoist it, and then it will see the second function declaration and hoist it similarly. It will write the second function in memory on top of the greetPerson function.

function greetPerson() {
  console.log("Hi");
}

console.log(greetPerson()); // 'Hi'

function greetPerson() {
  console.log("Hello");
}

Finally, look at the behavior of the let and const keywords introduced to our lives with ECMAScript 2015. In terms of hoisting:

console.log(person);
console.log(greetPerson);
console.log(greetPerson());

const person = "ahmet";

function greetPerson() {
  console.log("Hello " + person);

  let person = "mehmet";
}

The let, const, var, class, and function keywords are hoisted; however, due to the difference in initialization, the use of let and const in this way causes a ReferenceError. While a variable declared with let is uninitialized until the related statement is executed, a variable declared with var is also initialized with the undefined primitive.

console.log(person); // ReferenceError: person is not defined
console.log(greetPerson);
console.log(greetPerson());

const person = "ahmet";

function greetPerson() {
  console.log("Hello " + person);

  let person = "mehmet";
}

If we remove the first console statement, we will get the same error due to the execution of the greetPerson function caused by let.

* This article was initially published on labs.zingat.com on the date indicated.