There are countless articles related to the differences between let
, const
, and var
. However, when it comes to var
the usual conclusion is to just stop using it. I’m not here to debunk that or suggest a new way of doing things. But the interesting part is not whether it is abandoned or not, but rather knowing why. Why is that considered a good practice?
Well, the most important reason is the lexical scope.
You see, lexical scope and var
had a good life together. But one day… something changed. Lexical scope sought new horizons while var
remained in its comfort zone.
What you need to know
When talking about var we always have to talk about the past. In the old days, before the arrival of ECMAScript 6, var was the only way to declare variables. Well, you might say, “I already knew that, it’s one of the first things they tell you when they introduce let
and const
” – which is true. But what they don’t always tell you is that back then, lexical scope was divided only into two: global scope and local scope.
“Oh my god! What on earth are those scopes?”
Well, if you don’t know that, you shouldn’t be reading this. Go quickly to learn about lexical scope and then come back.
…
Do you know now? The answer should be yes, and therefore you already know the following information.
Scopes | ECMAScript 5 and earlier | ECMAScript 6 onwards |
block scope | No | Yes |
local scope | Yes | Yes |
global scope | Yes | Yes |
Keywords for declaring variables | ECMAScript 5 and earlier | ECMAScript 6 onwards |
let | No | Yes |
const | No | Yes |
var | Yes | Yes |
With this information in mind, you will understand the problem. Previously there was no block scope, and therefore var
was never designed to take that functionality into account.
Let’s see a couple of examples with var
and let
/const
.
let
and const
You can look at let
and const
as the successors of var
. Being the most recent ones it’s ideal to learn how they work first.
Of course, unlike var
both let
and const
behave properly with any of the 3 scopes.
Example 1
In the following code you will see a global variable and a local variable declared::
**Note: For the examples you will see, I will only use let
. However, I could have used const
and it would behave in the same way. When talking about scope they share the same behavior.
let outputPath = '../../resources/output'
function createNiceDocument() {
let niceOptions = {
margin: 'perfect',
textColor: 'darkGray',
}
console.log('outputPath: localScope = ', outputPath)
console.log('niceOptions: localScope = ', niceOptions)
//TODO: createDocument
}
createNiceDocument()
console.log('outputPath: globalScope = ', outputPath)
console.log('niceOptions: globalScope = ', niceOptions)
Upon execution, this code returns the following result:
outputPath: localScope = ../../resources/output
niceOptions: localScope = { margin: 'perfect', textColor: 'darkGray' }
outputPath: globalScope = ../../resources/output
...\src\var_let_const__lexicalScope\tempCodeRunnerFile.js:15
console.log('niceOptions: globalScope = ', niceOptions)
^
ReferenceError: niceOptions is not defined
As one would expect, the global and local scope are respected perfectly.
niceOptions
– the local variable- It can only be accessed within the local scope of the
createNiceDocument
function. - It throws an error if you try to call it in the global scope.
- It can only be accessed within the local scope of the
outputPath
– the global variable- It is called correctly regardless of whether you call it from the global scope or the local scope.
Example 2
Now. An example adding an if block:
let outputPath = '../../resources/output'
function createNiceDocument() {
let niceOptions = {
margin: 'perfect',
textColor: 'darkGray',
}
//Let's not make it complicated. The code will always enter this if statement.
if(true) {
let pagesNumber = 3
console.log('niceOptions: blockScope? = ', niceOptions)
console.log('pagesNumber: blockScope? = ', pagesNumber)
}
console.log('niceOptions: localScope = ', niceOptions)
console.log('pagesNumber: localScope = ', pagesNumber)
//TODO: createDocument
}
createNiceDocument()
It works as well:
pagesNumber
– the block variable- It can only be accessed within the block scope of the if statement inside the
createNiceDocument
function. - It throws an error if it is called either in the local scope of the function or in the global scope.
- It can only be accessed within the block scope of the if statement inside the
niceOptions
– the local variable- It is called correctly regardless of whether it is called from the local scope or the block scope.
niceOptions: blockScope? = { margin: 'perfect', textColor: 'darkGray' }
pagesNumber: blockScope? = 3
niceOptions: localScope = { margin: 'perfect', textColor: 'darkGray' }
...\src\var_let_const__lexicalScope\tempCodeRunnerFile.js:17
console.log('pagesNumber: localScope = ', pagesNumber)
^
ReferenceError: pagesNumber is not defined
var
Now it’s var'
s turn. How will the code behave in the previous examples if we replace let
with var
?
Example 1
First, let’s evaluate the scenario with the global and local scope:
var outputPath = '../../resources/output'
function createNiceDocument() {
var niceOptions = {
margin: 'perfect',
textColor: 'darkGray',
}
console.log('niceOptions: localScope = ', niceOptions)
//TODO: createDocument
}
createNiceDocument()
console.log('outputPath: globalScope = ', outputPath)
console.log('niceOptions: globalScope = ', niceOptions)
outputPath: localScope = ../../resources/output
niceOptions: localScope = { margin: 'perfect', textColor: 'darkGray' }
outputPath: globalScope = ../../resources/output
..\src\var_let_const__lexicalScope\tempCodeRunnerFile.js:15
console.log('niceOptions: globalScope = ', niceOptions)
^
ReferenceError: niceOptions is not defined
If you pay attention, there is no difference at all compared to the example with let
and const
. In these cases, you can use any of the 3 keywords interchangeably.
Example 2
Now, what happens in the scenario with the if statement?
var outputPath = '../../resources/output'
function createNiceDocument() {
var niceOptions = {
margin: 'perfect',
textColor: 'darkGray',
}
//Let's not make it complicated. The code will always enter this if statement.
if(true) {
var pagesNumber = 3
console.log('niceOptions: blockScope? = ', niceOptions)
console.log('pagesNumber: blockScope? = ', pagesNumber)
}
console.log('niceOptions: localScope = ', niceOptions)
console.log('pagesNumber: localScope = ', pagesNumber)
//TODO: createDocument
}
createNiceDocument()
If we stick to the idea that we should have a block scope, pagesNumber
should only be accessible within our if statement. Therefore, this code should throw an error:
niceOptions: blockScope? = { margin: 'perfect', textColor: 'darkGray' }
pagesNumber: blockScope? = 3
niceOptions: localScope = { margin: 'perfect', textColor: 'darkGray' }
pagesNumber: localScope = 3
…But that’s not the case. This is precisely because of what I mentioned before: var
doesn’t the take block scope into account and treats it as if it were a local scope. The way it achieves this is through a process called hoisting. (This is also an important topic, so make sure to note it down to learn about it later.)
This kind of behavior is the reason why it is recommended to use let
/const
instead of var
.
Regardless. there are still developers that see some value in using a var
. Though more than anything it is only used as a visual way to point that a variable is global.
I personally think this is a good idea. A simple visual way to identify a certain pattern on the codebase. But on the other hand I’m not radical about it either. You’ll be able to form you own opinion with experience.
As for me I’m happy as long as I don’t see var
declarations inside a function or even worse.. inside an if
, while
or for
statement.
BONUS
I think that a this point you should already know the difference between let
and const. Reading about Lexical Scope without knowing that is like opening a book and jumping ahead to chapter 10 instead of starting at 1.
But in the rare case you don’t know or forgot. I’ll remind you.
let
- You can modify the value after it has been initialized.
const
- You cannot modify the value after it has been initialized.