Surprisingly, my style of writing JavaScript code allows most variables to be declared as const
. One of the major rules I employ when writing my own code and helping others write theirs, is to create new variables with more precisely defined names instead of trying to reuse existing ones. The computational and memory costs of creating a variable are negligible, so clarity can and should be the focus. And yes, this is not what schools teach you to do with constant modifiers in any language. Consts tend to get special treatment. They're often treated as entrypoints for reconfiguring a program, moved to the top of code and given names in capital letters, so they're easy to discern. In JavaScript at least, I'd argue that const
s are far more useful.
But let's start from the most obvious outlier in the group - var
(I won't even discuss globals, because you should never use them). var
has been around for a long time and you can definitely write great scripts with it. It just doesn't reinforce good behaviors in programmers. I personally know developers and have read multiple discussions about how to use var
to keep your code readable and still working. The bottom line is, though, that to make var
as safe as possible, it has to be used at the very top of functions. Not code blocks, but functions. It's an important distinction. Otherwise, the following happens:
function do_something() {
console.log(bar); // undefined
var bar = 1;
}
This is called Variable Hoisting. var
statements are moved to the top of the function that contains them, but value assignments are done in the place they're written. It's therefore easier to accidentally end up using values too soon, because no error will be thrown where such code is present, even though it's clearly useless. This issue becomes even more apparent when using code blocks:
for(var p = 0; p < 10; p++){
buttonElements[p].addEventListener('click', function(){
toggleButton(buttonElements[p]);
});
}
toggleButton
will always be called with p
= 9, because p
is not local to every step in the iteration. It will be hoisted to the top of the function containing for
. I've made this mistake on occasion and have seen young programmers do it a bunch of times. It's very easy to miss. Both const
and let
don't have this issue. They work like constants and variables in most languages - they become available after the line where they're declared or desclared and assigned, so there's no harm in using the modifiers somewhere later in code. They are also local to blocks, so the loop shown earlier will work as expected if let
(only let
, since const
cannot change).
On top of all of this, my suggestion is to use const
by default and fall back to let
only if const
simply can't be used. Using const
makes it clearer that the value assigned to it won't change. To clarify, const
is not equivalent to immutable state. In Javascript, it works just like in most other programming languages. Objects can be created as constants but their contents can still be changed freely, because the reference to the object is made constant, not the contents of it. Making values constants is not a huge difference compared to making variables, but it can improve legibility a bit more, so I'm all for it.
One extra important thing to one at the end is how function declarations work in JavaScript. The following code works just fine:
p();
function p(){
console.log(new Error().stack);
}
The declared function as a whole will be hoisted to the top of the code which can be misleading, similarly as when using var
, except here p
is defined when it's called in the first line. But assigning p
to a variable can produce wildly different results.
p();
var w = function p(){
console.log(new Error().stack);
}
Now p
is undefined
when called and will still be undefined if w
and p
in the third line are switched. Changing var
to let
or const
will change nothing and force p
to be called below the function declaration. What I suggest doing is the same as with var
- avoid using function name(){}
alone and use const
(preferably) or let
. Running console.log(new Error().stack);
inside of the second function yields one more insight: error stacks reference p
and if it's not there, they use w
. This is very important for debugging. Calling the second function will return something similar to:
"Error
at p (mikohiqela.js:10:40)
at mikohiqela.js:13:1
at https://static.jsbin.com/js/prod/runner-4.0.4.min.js:1:13850
at https://static.jsbin.com/js/prod/runner-4.0.4.min.js:1:10792"
Using this stack message it's easy to figure out where the issue occured. Now let's take a look at a different piece of code with the same function implementation provided as the example:
(function(){
console.log(new Error().stack);
})();
Running this code will spit out:
"Error
at mikohiqela.js:10:40
at mikohiqela.js:11:3
at https://static.jsbin.com/js/prod/runner-4.0.4.min.js:1:13850
at https://static.jsbin.com/js/prod/runner-4.0.4.min.js:1:10792"
While it's possible to find out where the stack came from by looking at line numbers, in more complex scripts it can be very difficult to do so. Many libraries tend to return or run anonymous functions built using some builder functions, so that some values can be predeclared for them and they're easier to use. When following this pattern, let's make sure those anonymous functions are given a name somehow, so debugging is easier. To simplify even further, the following is also ok for debugging and will cause p
to be mentioned in the stack:
const p = () => {
console.log(new Error().stack);
}
p();
let
, var
and const
were recently mentioned by Douglas Crockford in his "The Post JavaScript Apocalypse" presentation and he came to similar conclusions about the use of each.