If you're a noob or a seasoned dev, you might look at code and look at recommendations for code style and wonder, why are these practices considered best practices?
Well, I hate to break it to you but majority of style preferences and best practices are more preferences and very subjectively "best". In fact, there are all kinds of "camps" in web development and JavaScript which occasionally clash on Twitter on something as mundane as the usage of a semicolon.
So, in this article, I'd like to tackle functions and variables to discuss their subjective "best practices" as well as reasons why not to use them.
Arrow Functions vs. Named Function
There are multiple ways of declaration a function. The arrow function lets you skip parenthesis in single-argument functions and the named function can be declared standalone or assigned to a variable. Here is a quick example:
function sum(leftNum, rightNum) {
return leftNum + rightNum;
}
const sum = (leftNum, rightNum) => {
return leftNum + rightNum;
}
Let's look at the feature differences of these two function declarations.
Hoisting
The first big one is hoisting. If you declare a named function outside of a scope block (like a do
or another function or a class), it'll automatically be available everywhere at run time! This allows you to structure your JavaScript file with a clear order without having to think about if a function will be available. For example:
bootstrapApplication(); // this runs without an issue
function bootstrapApplication() {
// logic here
}
But this doesn't work with arrow functions:
bootstrapApplication();
const bootstrapApplication = () => {
// logic goes here
}
In this example, the constant isn't available before it's declared.
If you check out my post on how I structure my javascript file, you'll see me taking advantage of it heavily and there are more examples.
The dreaded this
this
refers to the object you're currently writing code in. Whether that's an actual JavaScript object or a class, or a function expression, this
will let you access the prototype and it will let you access other internal variables.
Both named functions and function expressions create their own this
within its scope which can be confusing because you don't always want to do that.
// using function expression
function counterApp(arr) {
this.count = 0;
arr.forEach(function (item) {
this.count++; // which `this` is this?
});
return this.count;
}
// using named functions
function counterApp(arr) {
this.count = 0;
function namedFunction(item) {
this.count++; // is `this` available
}
arr.forEach(namedFunction);
return this.count;
}
In both cases, this
actually refers to the this
inside the function it's called. Eg. in the first example, this.count
would be undefined, and same with the second example.
Arrow function's rise in popularity stems from this problem. Back in the day, you'd assign this
to a variable: var self = this
and run self.count++
in both examples. In fact, this is how Babel and other popular tools transpile arrow functions.
Here's what you can do using arrow functions:
// using arrow function
function counterApp(arr) {
this.count = 0;
arr.forEach(item => {
this.count++; // which `this` is this?
});
return this.count;
}
// using arrow function assigned to a variable
function counterApp(arr) {
this.count = 0;
const namedFunction = (item) => {
this.count++;
};
arr.forEach(namedFunction);
return this.count;
}
In both of these example, this.count
is what you'd expect it to be and refers to counterApp
's internal count
variable.
The shorthand
One advantage that arrow functions have over named functions is that it has very neat shortcuts in writing them:
// single argument functions can drop the parentheses
const doubleNumber = num => {
return num * 2;
};
// any arrow function can drop the curly braces and return
// should the `return` be the only code in the function body
const multiplyNums = (num1, num2) => num1 * num2;
// this gets tricky if you're returning an object
// you can wrap the object in parentheses like so:
const initializeUser = userName => ({ name: userName, newUser: true });
Why use one or the other?
I've yet to meet a developer that will tell you to use one or the other in all cases. Instead, there are some best practices you can follow and I'll share those, just keep in mind that this is super subjective and most are personal preferences.
When to use arrow functions
// closures/callbacks
const userIds = users.map(user => user.id);
// when you need to access `this`
function application() {
this.count = 0;
const button = document.querySelector('.increment-button');
button.addEventListener('click', () => this.count++);
}
When to use named functions
// when hoisting is important
function applicationBootstrap(users) {
const userIds = users.map(getProperty('id'));
}
// gets hoisted and is available for `applicationBootstrap`
function getProperty(prop) {
// oooh, mixing things up!
return item => item[prop];
}
What about everywhere else?
It's all up to you! Seriously. Arrow functions or named functions in module exports? Your call.
Let vs Const vs Var
Oh no, oh no, oh no. I'm not touching this one...
Okay, I am. Especially since I used const
a bunch of times already.
NOTE Please assume I'm running everything in strict
mode.
Scope
A long time ago, we started with var
. var
does exactly just what it says: it declares a variable! That variable exists within a scope
. A scope
is usually a function (any type of function!), a module, or an object. Eg:
var hello = 'hi';
function welcome() {
var worldName = 'world';
console.log(hello, worldName); // accessible
}
console.log(worldName); // not accessible and undefined
Cool, keep this in mind. You can declare a variable within for loops and while loops and the variable will exist outside of them:
for (var i = 0; i < 10; i++) {
var finalCount = i;
}
console.log(finalCount); // accessible
console.log(i); // accessible as well!
Both let
and const
follow the scope rules with objects and functions but do not follow the rules of the for and while loops. So if you write an identical for loop:
let initialCount = 0;
for (let i = 0; i < 10; i++) {
let finalCount = i;
}
console.log(initialCount); // only one accessible
console.log(finalCount); // inaccessible
console.log(i); // inaccessible as well!
Variable assignment
Both var
and let
let you reassign a variable after initialization:
var initialized = false;
let userRole = 'read only';
if (userId === 1) {
userRole = 'admin';
}
initialized = true;
Both var
and let
are interchangeable in this example; however, const
is meant to be a constant
meaning that a variable cannot be reassigned once declared:
fucntion preciseNumber(num) {
const precision = 8;
if (num > 10) {
precision = 4; // this will throw an error
}
return num.toFixed(precision);
}
BUT, notice I say reassignment
, not mutation. What's the difference? If you assign an object or an array to a const
, you can manipulate it.
const user = {
id: 1,
name: 'Antonin Januska',
active: false,
monthsSinceLastActive: 3,
}
const inactiveUsers = [user];
if (user.monthsSinceLastActive < 10) {
user.active = true; // this works!
inactiveUsers.pop(); // works, too
}
user = getNextUser(); // fails because we're assigning a new object
inactiveUsers = [user]; // this would fail, too
When to use which?
This is a source of contention. Everyone has opinions.
var or let?
I typically don't see a reason to ever use var
and many developers would agree. Even in situations where var
would be useful (like getting the last value of a for loop), it's more explicit to write the workaround. Again, these are my personal opinions:
let lastValueLet = 0;
for (let i = 0; i < 10; i++) {
// I keep expecting this to error out
// because of redeclaration
var lastValueVar = i;
lastValueLet = i;
}
// I'd get tripped up by this, does it exist?
// what would be the value?
console.log(lastValueVar);
// very clear that it exists
console.log(lastValueLet);
Verdict: I default to let
whenever possible.
const?
Constants are even more controversial. One of the reasons is because of the object
gotcha where you can change the object despite being a constant. Remember, constants prevent reassignment, not mutation!
There are two camps/ideologies which I've seen regarding this:
True constants
Constant should be used only for numbers, strings, etc. but not objects. They should be used for declaring some kind of configuration or a mathematical constant (such as short-hand PI
):
const PI = 3.14;
function calculateArea(radius) {
return radius * PI * PI;
}
This camp usually recommends uppercasing constants which is something you'd do in other languages as well:
const MAX_HEALTH_VALUE = 100;
function drinkHealthPotion(currentHP, restoreAmount) {
if (currentHP + restoreAmount > MAX_HEALTH_VALUE) {
return MAX_HEALTH_VALUE;
}
return currentHP + restoreAmount;
}
"Don't touch" constants
With full knowledge of the constant caveats, the second camp is more concerned about the intent and recommend using const
to signify that you do not intent to change that variable. This is often used when destructuring arguments provided to a function or when declaration a function expression:
const sumFunctionExpression = (num1, num2) => num1 + num2;
function getUserId(user) {
const { id } = user;
return id;
}
Essentially, using const
is for anything and everything you don't want other developers to change in the future when they touch your code. And as an indicator to yourself that you don't want to change that variable.
This is my personal preferences.
Top comments (1)
Good article and well organized. As of
var
&let
I tend to usevar
when I want a variable to be visible in the whole application or scope and leavelet
for loops or variables inside functions. And for functions I only tend to use arrow/fat function whenever I want to use the global/localthis
.