Hoisting in JavaScript: It's Not Just Moving Code to the Top
We have a variable x = 7
, and we also have a function called getName
. This function simply logs a name.
var x = 7; function getName() { console.log("Jon Snow"); }
Now, let's invoke the function and log the value of x
:
var x = 7; function getName() { console.log("Jon Snow"); } getName(); console.log(x);
What do you expect to see here? You would expect getName
to print "Jon Snow" and x
to be printed as 7
.
Now, let me show you something interesting. It’s like magic in JavaScript! I'll move the function invocation and console.log
to the top of the program:
getName(); console.log(x); var x = 7; function getName() { console.log("Jon Snow"); }
What do you expect to see now? What will the output be?
What’s changed here is that we are trying to invoke getName
before it’s defined, and we’re trying to access x
before assigning a value to it. What do you think will happen?
In most programming languages, this would result in an error. You can’t access a variable before it’s initialized.
But JavaScript behaves differently in this case. When we run this code, you’ll see "Jon Snow" is printed. This means getName
somehow managed to access and invoke the function. But for console.log(x)
, we get a special value: undefined
.
Weird, right? Let's explore this further.
Suppose I remove the var x = 7
line completely:
getName(); console.log(x); function getName() { console.log("Jon Snow"); }
What do you expect to happen now?
This time, we get an error saying x
is not defined. Earlier, x
was undefined
, but now it's "not defined."
Isn’t undefined
and not defined
the same thing? Actually, no, they’re not!
This fascinating behavior is known as hoisting in JavaScript.
Hoisting is a phenomenon in JavaScript where you can access variables and functions even before they've been initialized or assigned a value. You can reference them without getting an error.
Let me show you something more interesting. Let’s log a function to the console:
var x = 7; function getName() { console.log("Jon Snow"); } console.log(getName);
We are not invoking the function; we are simply logging the function itself. What do you expect to see in the console?
It actually prints the function itself.
But what if we try logging the function before it's initialized?
console.log(getName); var x = 7; function getName() { console.log("Jon Snow"); }
Remember that when we logged x
, it returned undefined
. What do you think will happen with getName
?
It still gives us the function:
In the case of x
, it returned undefined
, but for the function, it logs the entire function.
This might seem strange, but it only feels that way because we don't fully understand what's happening behind the scenes.
Let’s dive deeper and explore why the program behaves this way and why everything happened the way it did.
Whenever we run a JavaScript program, an execution context is created, and this occurs in two phases. If you're not familiar with what an execution context is, how it's created, or what phases it involves, read this article before moving forward.
The concept of hoisting lies here in this first phase - Memory Creation phase.
Let’s go through each line of code step by step to understand how it works:
var x = 7; function getName() { console.log("Jon Snow"); } getName(); console.log(x); console.log(getName);
Before JavaScript starts executing the code, it allocates memory for all the variables and functions. So, even before the actual execution, a spot in memory is reserved for x
, and it’s given the value undefined
. For functions, the entire function is stored in memory.
This is why console.log(x)
outputs undefined
(since x
is declared but uninitialized) and getName()
logs "Jon Snow"—the entire function is available in memory.
The Difference Between undefined
and not defined
At this point, you might wonder about the difference between undefined
and not defined
in JavaScript.
If x
weren’t declared at all, trying to log it would cause a ReferenceError
, indicating that x
is "not defined" and doesn’t exist in memory. By contrast, undefined
means that x
exists in memory but hasn’t been given a specific value yet.
getName(); console.log(x); function getName() { console.log("Jon Snow"); }
In this case, because getName
exists in memory, JavaScript executes it without error. However, if x
weren’t declared anywhere in the program, trying to access it would result in a ReferenceError: x is not defined
.
Declaring getName
as an Arrow Function or Function Expression
Let's try another twist: what if we declare getName
as an arrow function instead?
getName(); console.log(x); var getName = () => { console.log("Jon Snow"); };
Here, you’ll encounter an error: "getName is not a function
." Why?
When getName
is an arrow function, it’s treated like a variable rather than a function declaration. During the memory creation phase, JavaScript sets getName
to undefined
, so when we try to invoke it, we get the error because undefined
isn’t callable.
The same result applies to function expressions:
getName(); console.log(x); var getName = function () { console.log("Jon Snow"); };
Here, too, getName
acts like a variable and is assigned undefined
at the start.
Key Takeaway
In summary, only function declarations (like function getName() {}
) load the entire function in memory during the memory creation phase. Arrow functions and function expressions act like variables, initialized to undefined
initially.
So, the next time someone asks what hoisting is, you'll know it’s much more than "moving code to the top." Hoisting reveals how JavaScript prepares your code by allocating memory in a unique way before execution.
Get my free, weekly JavaScript tutorials
Want to improve your JavaScript fluency?
Every week, I send a new full-length JavaScript article to thousands of developers. Learn about asynchronous programming, closures, and best practices — as well as general tips for software engineers.
Join today, and level up your JavaScript every Sunday!
Thank you, Taha, for your amazing newsletter. I’m really benefiting from the valuable insights and tips you share.