Every Tool has a Purpose

Ron Roe
7 min readNov 18, 2020

As a follow up to my previous story, Everything is Just a Tool, I wanted to address the litany of discussion around what to and not to use. Much of the work I do is in JavaScript, and it so happens that a greater portion of these articles are geared toward the language. And why wouldn’t they be? JavaScript isn’t just for the web on the front end. It has been described since its inception as the “language of the web”, but as time goes on, that becomes even less the case. Still, there are those who claim to represent the “best practices”, recommended by “top developers”. I believe there’s a better way to consider these concepts, and it begins with the same basic ideas I proposed previously. By way of example, I will discuss JavaScript variables and function definitions vs function expressions.

Declaring Variables

There are 4 primary ways to create variables in JavaScript. Some of the JS developers reading this are either wondering what the 4th method is, or fuming that I’d consider it at all. Variables may be declared using the keywords var, let or const, or with no keyword at all. Let’s look at the difference between them first.

var defines a variable in the current function’s scope. The variable is hoisted to the top of that scope, which means no matter where you define it, it’s as if you had declared it on the first line of the function. Its value can be reassigned at any time, and may be completely redeclared in the same scope or any other using the var keyword again. To visualize how a variable declared with var is scoped, we say that it is “scoped upward” to the nearest function block, or to global. By way of example, consider the following:

for(var i = 0; i < 5; i++){
var i = 4;
console.log(i);
}

For how many iterations does this loop run? Just one. The first time i is declared, it’s declared in the global scope. When it’s declared again inside the loop, it is still declared in the global scope. When i is incremented and then evaluated again, it’s equal to 5, so the loop does not continue. Incidentally, the output to the console is 4, because the variable is redeclared at that value.

let defines a variable in the scope of the current statement, and while its value may be altered, it cannot be declared again. Variables declared with let are not hoisted, so they cannot be used before they are declared. We might say that let variables are “scoped down”. Let’s use the same example, but use let to declare the variable inside the loop:

for(var i = 0; i < 5; i++){
let i = 4;
console.log(i);
}

For how many iterations does this loop run? Five. While var i's (let’s call this one “global i”) value is declared in the global scope, let i's value (let’s call this “local i”) is declared within the loop’s scope. In other words, they are different variables. They occupy different spaces in memory even though they have the same name. What is the output of this loop to the console? It is 4 five times. Each time the loop runs, it increments the global i, and then redeclares the local i. Another example:

for(let i = 0; i < 5; i++){
let i = 4;
console.log(i);
}

What happens with this loop? Well, the same thing, but for a slightly different reason. In the first line, let i declares the variable for the loop, and in the second line, it declares the variable within the loop. Scoping can get weird sometimes, but it’s important to understand that we are again declaring 2 separate variables that exist in different locations in memory. If we tried to redeclare them in the same scope, an error is thrown.

const variables are scoped in the same way as let variables, but cannot be changed in their scope at all. Let’s use the same example:

for(const i = 0; i < 5; i++){
const i = 4;
console.log(i);
}

How many iterations do we see this time? Just one. Why? Because we get an Assignment to constant variable error. But, what’s the output? We do see 4 logged to the console. This is because again, the variables are declared in a different scope.

Now, what happens when we don’t use a keyword at all? Remember when we used the var keyword, and the variable was declared in the global scope? Same thing here. I could go deeper into this one, but I can feel the eyes twitching at the mention of it. Give it a try inside a function, or mix and match the keywords with it as an experiment.

That was a lot of explanation just to come back around to a point. Current “best practices” dictate we not use var or global declarations at all. But why? Well, as we saw, the scoping rules cause some behaviors that can be problematic. Yet, both methods are still part of the current ECMAScript specification. For most things, you can get away with let and const. However, is there never a situation where we might want to declare using var? var has some very unique characteristics, characteristics that can be used to a developer’s advantage. Just because it can cause difficult behavior doesn’t mean it has to. Kyle Simpson, author of You Don’t Know JS, says it better than I can: “There are going to be places in real world code where some variables are going to be properly scoped to the entire function, and for those variables, var is a better signal”. In other words, maybe you won’t necessarily use the difference in scoping, but some times when a variable is used throughout a function, var may be more readable. It also sometimes makes sense programmatically to declare a variable in a way that intentionally makes use of var's behaviors.

Am I saying that you shouldn’t adhere to the recommended best practices? Not at all. Best practices are the best for a reason. They’re tested, tried and true patterns that make for better code over all. What I am saying, is don’t throw away the alternatives. They are still useful. Their behaviors can at times be preferable. This way of thinking can apply in a lot of cases, including how functions are declared.

Functions

There are more than two ways to declare a function in JavaScript, but for the sake of brevity, we’re going to look at only two and their differences and uses.

The function keyword is the syntax we are all first made familiar with. Not only is it generally taught early on in JavaScript courses, it’s also similar to other C-like languages, which makes it familiar. Like variables declared with var, functions declared with function are hoisted to the top of the nearest function block. Consider the following example:

init();
function init(){
console.log('initialized');
}

What happens when we run this code? "initialized" is logged to the console. That is because the function init is declared in this instance at the global scope.

Using the function expression, better known as “arrow function” syntax scopes the function similar to how let and const scope variables — even if you use the var keyword to declare it. Consider the following example:

init();
var init = () => console.log('init');

What happens here? We get an error that init is not a function.

Aside from scoping, the way functions are declared in JavaScript has scoping implications, especially for this. We’re not going to go super deep into this or how it works or what it’s for in this article. What’s important to note is that functions declared using the function definition syntax ( function keyword ) have their own this, and functions declared using the function expression syntax retain the this context of their parent function.

Current trends in best practices suggest that we should use the function expression syntax every time if possible. The best reason I can seem to find is to “avoid hoisting”. However, what if we wanted the hoisting. In modern JS projects, we have build pipelines and other tools that compile, minify and abstract the code we write. As such, it makes sense to modularize code as much as possible for readability purposes. However, it’s not always necessary or appropriate to integrate build tools or pipelines. Simple, one-off scripts to perform a task aren’t well served by that pattern, and it may be better to keep all of the code in a single file. How, then to retain readability? Keeping the “action code”, or the actual function calls and primary logic at the top of the file and the function definitions after enhances readability by allowing the reader to see how the script comes together first, and later break down into what happens under the hood. In this instance, hoisting is actually desirable.

And what of the this context? Again, the scope of this article does not include the ins and outs of this, but while it is generally desirable to maintain the parent this, there are cases when creating a local context is necessary — and as I stated before, the lesson here is to not throw patterns away entirely just because their behavior has quirks. As developers, it’s our job to know and utilize the tools of our trade to solve problems. When you disregard certain patterns or methods because someone told you it’s not the “best way”, you limit the size of your toolbox, and limit your capabilities as a developer.

Conclusion

Getting too hung up on what “top developers” do or consider best practice doesn’t add to your toolbox. In fact, it takes away from it. You’ll artificially restrict yourself from using tools at your disposal. There is a place for best practice in any profession, however it’s more important to understand just how your tools work, and how you can use them to overcome the problems you face as a developer. Every thing, every tool, every paradigm, every syntax has a purpose. If its purpose is meant to be replaced, it still has a use. Keep in the back of your mind how that tool works differently, because it just might be the one that solves that problem everyone’s stuck on.

--

--

Ron Roe

Software engineer and Air Force veteran. Helping engineers expand their skill sets and succeed in their careers