Hmmm, participating in that JavaScript boot-camp course was easy: you followed the instructions and wrote code word for word. Just like that paint-by-the-numbers activity. They tell you about variable and function hoisting, function declarations and expressions. Moreover, once ES6 (ECMAScript® 2015) syntax gets involved, the arrow functions appear. Eventually, you even use all of those in your sample project and it works! But do you really grasp, what is the difference between all those and practical use cases for each one?
In fact, my eye-opening moment happened when I did a test assignment for a potential job. It came to me in the form of:
Uncaught ReferenceError: Cannot access 'myFunction' before initialization
How so?! What about all the fancy function hoisting story?
Well, seems it is time to dive into the depths of JS and figure this one out. Hopefully, we will not end up all frustrated and knee-deep into the topics of this
and closures. For sure, there will be a better time for that.
Function declaration and function expression
You can define a function in multiple ways in JS. Each way results in a perfectly capable function, because in essence those are just different ways to create a Function object. According to MDN:
Function Declaration
Defines a function through a function statement. Syntax:
function myFunction (params){
//body of the function
}
Function declaration is hoisted to the top of local (or global) scope.
Function Expression
Defines a function inside an expression with function keyword. Syntax:
const myFunction = function (params){
//body of the function
}
Function expressions are not hoisted at all.
Function Hoisting
Turning to MDN again: JavaScript only hoists declarations, not initializations.
Indeed, this is the entire explanation on why function expressions are not hoisted. Let’s split up the above function expression to declaration and initialization parts:
//declaration part (notice the use of 'let' instead of 'const' here)
let myFunction;
//initialization part
myFunction = function (params){
//body of the function
}
This results in a situation, where function is hoisted by name, but will not return anything if called before the initialization part. A little practical demonstration to prove the point:
First, let’s attempt to call a function callEngineering()
before it is defined through a function expression. The result will give us a self-explanatory error: not defined, not hoisted, not available, end of story.
const response = document.querySelector('div');
try {
response.innerHTML = callEngineering();
} catch (e) {
response.innerHTML = `${e.name}: ${e.message}`
}
let callEngineering = function() {
return 'Scotty, we need more power!';
}
Output:
ReferenceError: Cannot access uninitialized variable.
Next up, calling a function after it is declared, but before it is initialized. This one will result in a hoisted callEngineering variable name, but without any content. It is not even known to be a function yet.
let callEngineering;
try {
response.innerHTML = callEngineering();
} catch (e) {
response.innerHTML = `${e.name}: ${e.message}`
}
callEngineering = function() {
return 'Scotty, we need more power!';
}
Output:
TypeError: callEngineering is not a function. (In 'callEngineering()', 'callEngineering' is undefined)
Finally, if we call the function after it was both declared and initialized, it makes total sense and returns the result we expect.
const response = document.querySelector('div');
let callEngineering = function() {
return 'Scotty, we need more power!';
}
try {
response.innerHTML = callEngineering();
} catch (e) {
response.innerHTML = `${e.name}: ${e.message}`
}
Output:
Scotty, we need more power!
For comparison, let’s define the callEngineering function through function statement and marvel the miraculous function hoisting in action.
const response = document.querySelector('div');
try {
response.innerHTML = callEngineering();
} catch (e) {
response.innerHTML = `${e.name}: ${e.message}`
}
function callEngineering () {
return 'Scotty, we need more power!';
}
Output:
Scotty, we need more power!
ES6: but what about arrow functions?
ES6 introduced us to a lean and more readable way to define a function. Truth be told, I was overusing this one heavily. However, to my despair function hoisting doesn’t work with arrow functions. All for one simple reason: In essence, arrow function is a syntactic sugar for function expression. So, as we figured above, it will not be hoisted.
Lesser-known: Function constructor
Another way to define a function is to use a Function constructor like so:
const myFunction = new Function('params', '//body of the function');
No hope for this one to be hoisted either. Same reason as above: it is not a function statement, but a function expression.
All in all, if you want to benefit from JavaScript function hoisting, keep an eye on the way you define your functions and structure your code accordingly.
LLAP!
Illustration by Shvets Anna