You can think of a scope as of a wrapper that contains all the variables and a reference to the parent scope. What does that mean and what should we watch out for when dealing with it? Let’s find out!
How scopes work
When we try to access a variable in our code, the interpreter first looks for it in the current scope. If it wasn’t found, it goes up through the chain of scopes. At the very top of it, there is the global scope. Variables declared there can be both accessed and altered anywhere in the code. Global scope exists as long as your application runs and there is only one instance of it. There is another type of scope called a local scope that can enclose the variables that are declared inside of it. It exists as long as it is executed. JavaScript uses Lexical Scope, which means that every level of the scope chain has an access to the outer levels.
Let
Let makes use of block scope. If you declare a variable with the let keyword, it will be accessible inside of a current block scope created with a block statement.
A block statement (or compound statement in other languages) is used to group zero or more statements.
We’ve all used it when writing if statements, or loops. Actually, you can even use standalone block statements.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
let pet = 'dog'; { pet = 'cat'; let catName = 'Garfield'; { let pet = 'lizard'; console.log(pet); // 'lizard' console.log(catName); // 'Garfield' } } console.log(pet); // 'cat'; console.log(catName); // Uncaught ReferenceError: catName is not defined |
In this example, when we tried to assign a value to a pet variable for the second time ( pet = 'cat'; ), it was not present in the current scope, so the chain of scopes was traversed and the pet with a value dog was found and replaced with a cat. That’s why the pet value at the very bottom is a cat.
When the variable named pet was assigned a value for the third time ( let pet = 'lizard'; ), the chain of scopes did not need to be traversed, because the variable named pet was declared with a let keyword in the current scope. That’s why the pet value at the very bottom is not a lizard.
Const
Const behaves the same as let when it comes to scopes. It is, as the name suggests, a constant.
- You need to initialize it with a value when declaring
-
1const variableName; // Uncaught SyntaxError: Missing initializer in const declaration
-
- You can’t change its value
-
12const aNumber = 10;aNumber = 10; // Uncaught TypeError: Assignment to constant variable.
-
There is a thing to keep in mind though when it comes to the second point. If you assign an object to the variable, it actually stores just a reference to it. It means, that if you assign an object to a const variable, you can change its properties.
1 2 3 4 5 6 7 8 |
const pet = { type: 'Dog' } pet.type = 'Cat'; // no errors pet = { type: 'Cat' } // Uncaught TypeError: Assignment to constant variable. |
Var
Back in the old days (before ES6) the only type of variable declaration keyword we had was var. Variables declared with var are function-scoped. It means, that declaring a variable with var makes it accessible throughout the whole function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// Global scope var pet = 'dog'; var dogName = 'Fluffy'; (function firstFunction() { // Local Scope no 1 var pet = 'cat'; var catName = 'Garfield'; (function secondFunction() { // Local Scope no 2 var pet = 'lizard'; console.log(pet); // 'lizard' console.log(dogName); // 'Fluffy' console.log(catName); // 'Garfield' console.log(window.pet) // 'dog' })() })() |
If this looks confusing to you, I used Immediately Invoked Function Expression (IIFE) here.
Global variables declared with the var keyword are attached to the window object (or the global object, if you are using Node.js). Even if you declare variables at the bottom of the function, they are going to be hoisted to the top (it does not happen with let and const).
1 2 3 4 5 6 7 |
(function() { console.log(dog); // undefined console.log(cat); // Uncaught ReferenceError: cat is not defined var dog = 'Fluffy'; console.log(dog); // 'Fluffy' })() |
When you see var dog = 'Fluffy'; you might think of that as one statement. JavaScript splits it in two: var dog; and dog = 'Fluffy'; . The first one will run before interpreting your code. Actually, both variables and functions are processed first, before any part of your code is executed.
There is a difference between the variable having a value of undefined, and not being defined, as the latter causes an error that stops the process of interpreting the code. It means that ‘Fluffy’ will not appear in the console at all!
Not using any keyword
1 2 3 4 |
(function(){ dog = 'Fluffy'; })() console.log(window.dog); // 'Fluffy' |
If you don’t use any keyword at all when declaring a variable, the interpreter will traverse the scope chain just like in an ordinary value assignment. The only difference is that it won’t find a variable named ‘dog’ and it will create a global variable. This is considered a bad practice and is discouraged. It won’t work at all when in strict mode and will result in an error
Uncaught ReferenceError: dog is not defined
Other occurrences of block scope
There were a few situations where the block scope existed, even before ES6. ES3 specified the variable declaration in the catch clause of a try/catch to be block-scoped to the catch block.
1 2 3 4 5 6 |
try { JSON.parse(); // will cause an error } catch (err) { console.log(err); // SyntaxError: Unexpected token u in JSON at position 0 } console.log(err); // ReferenceError: err is not defined |
Temporal Dead Zone
There is a catch to keep in mind when trying to access variables that might not have been defined yet. The only way to safely check if a variable was defined was to use typeof operator. Unfortunately, it causes problems with const and let variables.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
if (typeof a === 'undefined') { // a is not declared, works as intended } if (typeof b === 'undefined') { // declaration of b is hoisted to the top, no worries there } if (typeof c === 'undefined') { // Uncaught ReferenceError: c is not defined } var b; let c; |
ReferenceError from accessing let and const variables too early is called Temporal Dead Zone. Unfortunately, typeof behaves differently in this case and it is no longer safe to use it. It might be a good idea to declare let variables at the very top of the block to avoid it.
Let & for
Let has a special behaviour used in the head of a for-loop. It will cause variables declared in the head of the for loop to be declared for each iteration, not just once. Every time it will be initialized with the value from the end of the previous iteration.
1 2 3 4 5 6 7 |
const functions = []; for (let index = 0; index < 5; ++index) { functions.push(() => index); } functions.forEach(fn => console.log(fn())); // 0 1 2 3 4 |
Each index variable refers to the binding of one specific iteration and preserves the value. Therefore, each arrow function returns a different value. When it comes to var, the index variable will be declared once, and then modified.
1 2 3 4 5 6 7 |
const functions = []; for (var index = 0; index < 5; ++index) { functions.push(() => index); } functions.forEach(fn => console.log(fn())); // 5 5 5 5 |
Summary
In the current state of JavaScript, there is no reason to use var anymore, really. You should always use const if possible. If not, use let. This will help make your code readable, predictable and clean.
Great article! Thanks a lot 🙂
Great article described differents between let and var. Thanks for that! Greetings.