var, let, and const
I have another long post for you today about a fundamental concept in JavaScript: Variables. Or more specifically, variables when they are declared with the keywords var
, let
, and const
.
Before we get into the nuances of these three keywords, I think it's important to understand what variables are in programming. I like to think of them as little buckets that you can put information into. The bucket carries the value it's given, so that value can be transported around a program and used wherever it may be needed. Think about the following code:
var someNumber = 19258263202585;
function doubleNumber(num) { return num * 2;}
doubleNumber(someNumber);// expected result: 38516526405170
In this example we've declared a variable, someNumber
and given it the value 19258263202585
. We then create a function doubleNumber()
which accepts a number, num
, as its sole parameter, and returns num
multiplied by 2. We then call doubleNumber
, passing it someNumber
as its argument. If we run this code, we get 38516526405170
.
This is some trivial code, but it demonstrates how a variable can be declared and used in a little JavaScript program. In my mind, that random number is going to be impossible to remember when I use it later, but by saving it to a common English variable name like someNumber
, it becomes easier to work with down the line.
In practice, variables are typically used in one of two main ways in programming:
- To track state across the program's lifecycle
- To store constant values
Tracking State
Observe the following code:
var ovenOn = false;var ovenTemperature = 70;
function heatOven(targetTemperature) { if (ovenOn === false) { ovenOn = true; console.log("Oven has been turned on."); }
while (ovenTemperature < targetTemperature) { ovenTemperature += 1; }
console.log(`Oven has been heated to ${ovenTemperature}.`);}
function turnOvenOff() { if (ovenOn === true) { ovenOn = false; } console.log("Oven has been turned off.");}
heatOven(350);// expected result:// Oven has been turned on.// Oven has been heated to 350.
turnOvenOff();// expected result: Oven has been turned off.
Here we have a simple program for turning an oven on, heating it to a desired temperature, and then turning it off.
We start by creating two variables, ovenOn
and ovenTemperature
. ovenOn
is initially set to false
, indicating that the oven is not on by default. ovenTemperature
is initially set to 70
, as a resting oven will typically be around room temperature.
We then have two functions.
The first function, heatOven()
, accepts a targetTemperature
parameter. It checks whether the oven is off, and if so, it turns the oven on by changing the value of the ovenOn
variable to true
. Then it increments the ovenTemperature
until the ovenTemperature
is no longer less than the targetTemperature
.
The second function, turnOvenOff()
checks whether the oven is on, and if so, it turns the oven off by changing the value of the ovenOn
variable to false
.
We then call the two functions (and give heatOven()
a targetTemperature
argument value of 350
). We get the following logged out to the console:
Oven has been turned on.
Oven has been heated to 350.
Oven has been turned off.
This is a fairly naive and poorly written program, but it demonstrates how we can use variables to track state across a program. The ovenOn
variable tracks whether the oven is on or off. This could be analogous to something like whether a user is logged in on your website - maybe you want to display different content to a logged in user than a user that is not logged in.
The ovenTemperature
variable tracks the temperature of the oven in relation to a target temperature. This might be analogous to a shopping cart total. Perhaps you'll offer free shipping if the shopping cart total is greater than $350.
The point here is that we can use variables to keep track of various changing components of our programs and applications, and that doing so is called tracking the program's state.
Storing Constant Values
The other main use case for variables in programming is to store constant values. Consider the following code:
var TAX_RATE = 0.08;
function calculateTotal(subtotal) { return subtotal + subtotal * TAX_RATE;}
calculateTotal(100);// expected result: 108
calculateTotal(162);// expected result: 174.96
We start this code example by defining a variable, TAX_RATE
, with the value of .08
. We then create a function calculateTotal()
that accepts a subtotal
parameter, and applies the defined tax rate to that subtotal.
In this case the tax rate won't change, but we don't want to code .08
into the places where we would use that tax rate, so we define it as a variable and use the variable instead. A common practice with constant variables is to name them using all caps, which is what I have done here.
So unsurprisingly when we call calculateTotal()
with the values 100
and 162
, we get a .08
tax rate applied, giving us 108
and 174.96
respectively.
This is a small example, but if you had a more elaborate shopping cart application you might be able to see how you would want to use the TAX_RATE
variable in multiple places throughout the program. This helps to futureproof your application, because if the tax rate were to change, you would (ideally) only need to update that single variable, and the change would apply everywhere that variable is used. This kind of single-sourcing is incredibly powerful, and it demonstrates how we can use a variable to store constant values that will get reused throughout a program.
var
You've seen me use a keyword, var
, in the examples above to define variables. var
is the standard keyword that gets used to initialize variables in JavaScript, but under the hood there's a lot going on when we use that keyword. Let's take a look at the following code:
var test = "Hello";console.log(test);// expected result: "Hello"console.log(window.test);// expected result: "Hello"
In a base context like this, declaring a variable with var
creates a property at the global scope with the provided name. So test
and window.test
return the same value, "Hello"
.
But by default JavaScript allows us to declare variables without using the var
keyword. Here's a similar example, but with the var
keyword omitted:
test1 = "Hey";console.log(test1);// expected result: "Hey"console.log(window.test1);// expected result: "Hey"
It would seem that we get the same kind of behavior as using var
, but that's not actually the case. If we do a bit of a comparison:
var test = "Hello";test1 = "Hey";
Object.getOwnPropertyDescriptor(window, "test");/* * expected result: * { * value: "Hello", * writable: true, * enumerable: true, * configurable: false * } */Object.getOwnPropertyDescriptor(window, "test1");/* * expected result: * { * value: "Hey", * writable: true, * enumerable: true, * configurable: true * } */
Object.getOwnPropertyDescriptor()
allows us to dig into the Descriptor for each variable by passing it the object and the variable/property name on that object. This Descriptor is itself an object that describes the configuration of its variable, with properties showing its value
, and whether it is writeable
, enumerable
, or configurable
.
The key difference here is that when we use var
to declare a variable, configurable
is set to false
. If we omit var
, then configurable
is set to true
. This is important, because if configurable
is set to true
, that means that the other attributes of the variable can be modified and it can be deleted from its parent object:
var test = "Hello";test1 = "Hey";
console.log(test);// expected result: "Hello"console.log(test1);// expected result: "Hey"
delete window.test;delete window.test1;
console.log(test);// expected result: "Hello" - was not deletedconsole.log(test1);// expected result: ReferenceError: test1 is not defined
You'll observe similar behavior if you define the variable name as a property directly on the window
:
window.test2 = "Howdy";console.log(test2);// expected result: "Howdy"console.log(window.test2);// expected result: "Howdy"
delete window.test2;
console.log(test2);// expected result: ReferenceError: test2 is not defined
All of this is a very long way of saying that this...
var test = "Hello";
...is pretty much the same as:
Object.defineProperty(window, "test", { value: "Hello", writable: true, enumerable: true, configurable: false,});
And that this...
test1 = "Hey";
...is pretty much the same as:
Object.defineProperty(window, "test1", { value: "Hey", writable: true, enumerable: true, configurable: true,});
Using var
creates the namespace for a non-configurable object with the given value. Omitting var
creates the namespace for a configurable value. This means its properties can be modified and the variable can be deleted.
Being able to easily create configurable objects is considered an anti-pattern. Imagine the trouble you would be in if somewhere in a program you were writing you reached for an important variable, only to find that that variable didn't exist because at some point it was unexpectedly deleted! That is why when you are working in Strict Mode you are forbidden from creating variables without using a declaration keyword like var
, let
, or const
:
"use strict";test1 = "Hey";// expected result: ReferenceError
var
Variable Scope
In each of the examples above, we are declaring our variables using var
outside of any functions. Doing so means we are creating these variables at the global scope. If you create a globally scoped variable in a browser, you can access that variable as a property on the window
:
var test = "Hello";console.log(test);// expected result: "Hello"console.log(window.test);// expected result: "Hello"
If, however, you declare a variable using var
within a function then it is scoped to that function, and not necessarily available outside of the function:
function sayHello() { var greeting = "Hello"; console.log(greeting);}
sayHello();// expected result: "Hello"
console.log(greeting);// expected result: ReferenceError: greeting is not defined
In this code we define a function, sayHello()
, and within that function define a variable, greeting
using the var
keyword. We then console.log(greeting);
.
Below the function definition we call the function, and the result is that "Hello"
gets logged to the console.
Below that, outside of the function, we console.log(greeting);
. We get a ReferenceError
telling us that greeting
is not defined. This happens because we're asking for a variable at the global level when that variable is scoped to the sayHello()
function.
So what happens when we have both a globally scoped var
variable and a function scoped var
variable of the same name?
var greeting = "Howdy";
function sayHello() { var greeting = "Hello"; console.log(greeting);}
sayHello();// expected result: "Hello"
console.log(greeting);// expected result: "Howdy"
In this instance we have created a variable, greeting
, at the global scope and assigned it the value of the String "Howdy"
, then created a function, sayHello()
. Inside of that function we have created another variable named greeting
with the value of the String "Hello"
. This variable is scoped locally to the function in which it was declared. This function simply logs the value of greeting
.
We then run the function, which logs out "Hello"
, i.e. the value of its locally/function scoped variable. After that we log the value of the global greeting
value, and get "Howdy"
. You might expect that running the sayHello()
function would reassign the value of greeting
, but that's not the case as the two instances of the greeting
function have separate scopes owing to how we are using the var
keyword here.
Now let's look at the same code, but omit the var
keyword inside of the function:
var greeting = "Howdy";
function sayHello() { greeting = "Hello"; console.log(greeting);}
sayHello();// expected result: "Hello"
console.log(greeting);// expected result: "Hello"
This change means that we are now working with the value of the greeting
function that was declared globally. So the value of the variable we observe in the console is the same whether we run the function or log it directly after running the function, because within the function we are assigning a new value to the globally scoped variable.
Scoping is an important concept when we work with variables. Not understanding where a variable will be scoped can cause scope pollution. Consider the following little for
Loop:
for (var i = 0; i < 3; i++) { console.log(i); // 0, 1, 2}console.log(i); // 3
Here we have created a for
Loop, with a counter variable, i
, initialized at 0
. The loop will run so long as i
is less than 3
, incrementing i
on each iteration. Within the loop block we are logging the value of i
to the console. This gives us 0
, 1
, and 2
.
However, we also log i
's value to the console outside of the loop after the loop has run, and we get the value 3
. This happens because the i
variable as we have declared it is scoped locally to the environment in which this loop has been written, in this case that means globally. So now i
exists as a variable outside of its loop. On the last iteration of the loop, i
gets incremented from 2
to 3
. At this point the condition of the loop is no longer met, but a variable i
with the value of 3
still exists globally even though it's no longer needed. We saw this as a major issue with the code from my last post, Frontend JavaScript Pop Quiz. There is an easy fix to avoid polluting the scope with an unnecessary variable, but we'll get to that later.
var
Hoisting
Another important concept that relates to variables is the idea of Hoisting. Consider the following code:
console.log(greeting);// expected result: undefined
var greeting = "Howdy";
In this example, the first thing we are doing is logging the value of the greeting
variable to the console. We're explicitly doing this before we declare the variable using the var
keyword, which happens below the console.log()
statement, at the same time that we're assigning the value of the String "Howdy"
to the greeting
variable.
There are a few important things to note about this: first, when we attempt to access the greeting
variable before it's both defined and assigned a value, we observe that it has an undefined
value. This happens because before JavaScript executes its code, the compiler hoists or moves variables to the top of their scope. So the way the compiler is actually treating this code is something like the following:
var greeting;console.log(greeting); // we get the undefined value because we're accessing the variable heregreeting = "Howdy";
It's important to note that this behavior only works with variables that are actually declared using the var
keyword. If we try to do the same thing but omit the var
keyword (something we've already established is an anti-pattern that should be avoided!), we won't get undefined
but instead we get a ReferenceError
:
console.log(greeting);// expected result: ReferenceError: greeting is not defined
greeting = "Howdy";
Reassigning a var
Variable's Value
Something we've observed in our previous examples but that is worth calling out on its own is that var
variables are allowed to be reassigned new values. Consider the following code:
var greeting = "Howdy";greeting = "Hello";console.log(greeting);// expected result: "Hello"
We are declaring a variable, greeting
, using var
and assigning its value as the String "Howdy"
. We then immediately reassign it the String value "Hello"
. This will now be its value whenever we access it thereafter unless it gets reassigned again. This behavior is crucial in certain circumstances, like using a var
variable as a counter in a for
Loop, or allowing a value to change as a program runs. Being able to adjust a var
variable's value is what allows us to use var
variables to track state across the lifecycle of a program.
Initializing a var
Variable without a Value
Sometimes we know we'll need a variable but don't necessarily want to assign it a value right away. Since we can reassign the value of a var
variable as needed, we're able to initialize a var
variable without immediately assigning it a value:
var gasolineCar = { make: "Toyota", model: "Camry", type: "gasoline",};
var electricCar = { make: "Toyota", model: "Prius", type: "electric",};
function determineMileage(vehicle) { var mileage; if (vehicle.type === "gasoline") { mileage = "not as far"; } if (vehicle.type === "electric") { mileage = "really far"; } return `The ${vehicle.make} ${vehicle.model} will travel ${mileage}.`;}
determineMileage(gasolineCar);// "The Toyota Camry will travel not as far."
determineMileage(electricCar);// "The Toyota Prius will travel really far."
You'll see that within the determineMileage
function we declare a variable, mileage
, but don't assign it a value immediately. Instead we assign it a value based on one of two conditions - whether vehicle.type
is "gasoline"
or "electric"
.
Redeclaring a var
Variable
Lastly, if we declare and assign a variable using var
, we can use var
again later on to redeclare that variable later with a different value:
var greeting = "Howdy";var greeting = "Hello";console.log(greeting);// expected result: "Hello"
You'll need to be careful redeclaring var
variables within the same global or functional scope though, because you may get some unintended side effects:
var greeting = "Howdy";
if (true) { var greeting = "Hello"; console.log(greeting); // expected result: "Hello"}
console.log(greeting);// expected result: "Hello"
You might expect the second greeting
variable declaration and assignment to only apply within the if
block, but that's not the case. Remember that var
variables are either globally- or function scoped. In this case, the two var greeting = ...
declaration statements are working in the global scope, so even though the second declaration is inside of a conditional statement, it overrides the first throughout the global scope.
One way to get the expected outcome here is to do a function declaration:
var greeting = "Howdy";
function someFunction(boolean) { if (boolean === true) { var greeting = "Hello"; } console.log(greeting);}
someFunction(true);// expected result: "Hello"
console.log(greeting);// expected result: "Howdy"
Similarly, we could use an Immediately Invoked Function Expression (IIFE):
var greeting = "Howdy";
(function someFunction() { var greeting = "Hello"; console.log(greeting);})();// expected result: "Hello"
console.log(greeting);// expected result: "Howdy"
But either of these options are...messy. We'll see a better way to accomplish this behavior later.
So we have dug really deep into var
variables across five different behaviors:
- Scope
- Hoisting
- Reassigning the Variable's Value
- Initializing the Variable without a Value
- Redeclaring the Variable
We've also observed some of the key differences between declaring a variable with the var
keyword and omitting the var
keyword. The big takeaway on this point is that omitting the var
keyword can cause some unexpected and undesired behavior. This technique should be avoided, and is in fact considered an error when working in Strict Mode.
For a very long time var
was the only keyword available for declaring variables, and all of the behavior above was simply the standard way of working with variables in JavaScript. ES6 brought us two new keywords, let
and const
, which can also be used to declare variables. They carry with them different rules across those same five behaviors, so let's break them down on their own. At the end of this post I will have a summary section that directly compares var
, let
, and const
.
Let's explore let
first.
let
let
is one of the variable declaration keywords introduced in ES6.
let
Variable Scope
Like var
variables, when you declare a variable using the let
keyword at the top-level if your program outside of any other context, it is globally scoped:
let test = "Hello";console.log(test);// expected result: "Hello"
However, unlike var
variables, globally scoped let
variables are not available as properties on the global object/window
:
let test = "Hello";console.log(window.test);// expected result: undefined
Understanding the reasons for this involve digging into object environment records vs. declarative environment records. The global environment record is a composite of these two things. However, these topics are a bit beyond the scope of this blog post, and I will say that it suffices to simply understand that globally scoped let
(and const
) variables are not accessible through the global object/window
. There is a Stack Overflow thread on this topic here if you would like to read more.
The other big scoping difference between let
(and const
) variables and var
variables is that where var
variables are either global or function scoped, let
and const
variables are block scoped. A block is anything inside of opening and closing curly brackets, {}
.
Consider the following code:
var x = 100;
if (x === 100) { var y = 200;}
console.log(y);// expected result: 200
In the code above, the y
variable is available outside of the if
statement code block, since var
variables are scoped either globally or to the function in which they are declared. Now let's change that var
to let
:
var x = 100;
if (x === 100) { let y = 200;}
console.log(y);// expected result: ReferenceError: y is undefined
We get a ReferenceError
telling us that y
is undefined, because we are asking for the value of a let
variable outside of the block that it was defined in. The y
variable is now only available within the if
statement's curly bracket block:
var x = 100;
if (x === 100) { let y = 200; console.log(y); // expected result: 200}
If we revisit our for
Loop example from above, we can now fix the variable pollution issue we observed:
for (var i = 0; i < 3; i++) { console.log(i); // 0, 1, 2}console.log(i); // 3
If we change var i = 0
to let i = 0
...
for (let i = 0; i < 3; i++) { console.log(i); // 0, 1, 2}console.log(i); // ReferenceError: i is not defined
...then our loop runs as many times as we intend it to, but we don't pollute the global scope outside of the loop with an unnecessary variable. This let
variable is scoped directly to the {}
loop block! (Nuance here: the loop is actually creating a new i
variable for each iteration of the loop, and it only exists within the block of the loop. A var
variable gets declared in the outer scope. See the let
+ for
section of Chapter 2: Syntax within Kyle Simpson's You Don't Know JS: ES6 & Beyond (1st edition)).
let
Variable Hoisting
You'll recall that var
variables will get hoisted, allowing their declared name to be accessed, but not its value. Not so with let
variables.
console.log(greeting);// expected result: ReferenceError: greeting is not defined
let greeting = "Howdy";
let
variables do not permit hoisting at all.
This behavior is also the same within a block:
{ console.log(greeting); // expected result: ReferenceError: Cannot access "greeting" before initialization
let greeting = "Howdy from inside of a block";}
This behavior relates to something called the Temporal Dead Zone ("TDZ"). The TDZ is specific for each let
variable, and extends from the start of the block that it is declared in through the end of its declaration:
{ // TDZ for greeting starts here console.log(greeting); // ReferenceError
let greeting = "Howdy from inside of a block"; // End of TDZ for greeting}
You can read more about the Temporal Dead Zone and its nuances in this article on MDN.
Reassigning a let
Variable Value
Similar to var
, you are permitted to reassign the value of a let
variable:
{ let greeting = "Howdy"; greeting = "Hello"; console.log(greeting); // expected result: "Hello"}
Initializing a let
Variable without a Value
Along the lines of being able to reassign a let
variable's value, you are also permitted to initialize a let
variable without a value and assign it a value at a later point in the program:
{ let greeting; greeting = "Hey there"; console.log(greeting); // expected result: "Hey there"}
This enables us to assign the value of the variable programmatically if necessary, as in the example above with the electric and gasoline vehicles.
Redeclaring a let
Variable
You'll also remember that you are allowed to redeclare var
variables:
var greeting = "Howdy";var greeting = "Hello";console.log(greeting);// expected result: "Hello"
However you are not allowed to redeclare let
variables in this manner:
{ let greeting = "Howdy"; let greeting = "Hello"; // expected result: SyntaxError: Identifier "greeting" has already been declared}
This however does not apply to let
variables declared in different scopes, as they exist in separate lexical scopes:
let greeting = "Howdy";
if (true) { let greeting = "Hello"; console.log(greeting); // expected result: "Hello"}
console.log(greeting);// expected result: "Howdy"
In this way we can leverage the power of block-scoping to truly redeclare a greeting
variable scoped to the condition block and not outside of it.
Redeclaring a let
variable with var
On the topic of redeclaring let
variables, it should also be noted that you are not permitted to redeclare a let
variable using the var
keyword:
let greeting = "Hello";var greeting = "Howdy";// expected result: SyntaxError: Identifier "greeting" has already been declared
The opposite also doesn't work:
var greeting = "Howdy";let greeting = "Hello";// expected result: SyntaxError: Identifier "greeting" has already been declared
However, if the let
variable is within its own block...
var greeting = "Howdy";{ let greeting = "Hello"; console.log(greeting); // expected result: "Hello"}
console.log(greeting);// expected result: "Howdy"
...then we have no issues.
In fact, this combination of var
variables being globally- and function scoped and let
variables being globally- and block scoped is a powerful one. So long as you can abide by the correct rules for redeclaration and reassignment, you have at your fingertips the ability to control the value of these variables at a granular level throughout your program:
var greeting1 = "Hello";var greeting2 = "Howdy";
if (greeting1 === "Hello") { var greeting1 = "Hey"; // globally scoped let greeting2 = "Hi there"; // scoped to this if-block
console.log(greeting1); // expected result: "Hey" console.log(greeting2); // expected result: "Hi there"}
console.log(greeting1);// expected result: "Hey"console.log(greeting2);// expected result: "Howdy"
You may be wondering, "Hey what about all of that Object.getOwnPropertyDescriptor()
stuff we saw on var
variables? Are let
variables configurable, enumerable, and writable???" To be completely honest, that's a great question. I don't think that let
(or const
) variable descriptors are made available. Part of using the getOwnPropertyDescriptor
Object method is knowing the object that the variable is a property of, and as discussed earlier, let
and const
variables exist within a declarative environment record, a data structure that I can't figure out how to access.
I think that configurability, enumerability, and writability are not as much of concerns for let
and const
variables since they are so drastically different from var
variables. The reason that a var
variable's configurability even comes up is because the language allows us to declare similar variables without the var
keyword outside of strict mode. I don't think that it's possible to declare anything like a let
or const
variable without using those keywords, so that particular point of confusion just isn't an issue.
Do you understand the nuance of this topic better though? I would be so happy to reach some clarity here! Please reach out to me at joey@joeyreyes.dev, and I will gladly put any info you can provide here with credit.
Anyway, onto const
variables!
const
const
is the other variable declaration keyword introduced in ES6. It is used to denote constants, or variables whose values will not change. As such, it cannot be reassigned (although there's a big caveat when it comes to object properties and array values) and it cannot be initialized without a value. Otherwise, a const
variable follows all other rules that let
variables follow.
Think back to the following example:
var TAX_RATE = 0.08;
function calculateTotal(subtotal) { return subtotal + subtotal * TAX_RATE;}
calculateTotal(100);// expected result: 108
calculateTotal(162);// expected result: 174.96
We could easily do the same with a const
variable:
const TAX_RATE = 0.08;
function calculateTotal(subtotal) { return subtotal + subtotal * TAX_RATE;}
calculateTotal(100);// expected result: 108
calculateTotal(162);// expected result: 174.96
const
variables convey the intent that the value of your variable will not change. It's a clean, simple way to communicate what a variable is doing to other developers (or your future self) who work on your code. As such it is a valuable tool for our second use case for variables, which is storing constant values.
const
Variable Scope
Like let
, const
variables are either globally scoped, but not available as properties on the global object...
const test = "Hello";console.log(test);// expected result: "Hello"
console.log(window.test);// expected result: undefined
...otherwise they are block scoped:
var x = 100;
if (x === 100) { const y = 200; console.log(y); // expected result: 200}console.log(y);// ReferenceError: y is not defined
const
Variable Hoisting
Also like let
variables, const
variables cannot be hoisted, and follow the same Temporal Dead Zone rules:
{ // TDZ for greeting starts here console.log(greeting); // ReferenceError: Cannot access "greeting" before initialization
const greeting = "Howdy from inside of a block"; // End of TDZ for greeting}
Reassigning a const
Variable Value
However, unlike let
variables, you cannot reassign a new value to a const
variable:
let greeting1 = "Howdy";greeting1 = "Hello";console.log(greeting1);// expected result: "Hello"
const greeting2 = "Hi";greeting2 = "Hey";// expected result: TypeError: Assignment to constant variable
This behavior is unique to const
variables. However, underneath the hood, a const
variable declaration is creating a read-only reference to a value, which means that the value itself isn't immutable, just that the variable identifier cannot be reassigned. This means that if the value is an object, its properties can still be changed, or if the value is an array, its elements can be changed.
const
Variables and Object Properties
So where this isn't allowed:
const gasolineCar = { make: "Toyota", model: "Camry", type: "gasoline",};
gasolineCar = "A Gasoline-Powered Toyota Camry";// TypeError: Assignment to constant variable.
This is:
const gasolineCar = { make: "Toyota", model: "Camry", type: "gasoline",};
gasolineCar.model = "Echo";console.log(gasolineCar);/* * expected result: * { * make: "Toyota", * model: "Echo", * type: "gasoline", * } */
You are also permitted to add new properties to a const
variable object:
const gasolineCar = { make: "Toyota", model: "Camry", type: "gasoline",};
gasolineCar.model = "Echo";gasolineCar.year = 2010;console.log(gasolineCar);/* * expected result: * { * make: "Toyota", * model: "Echo", * type: "gasoline", * year: 2010, * } */
In order to really ensure that an object's properties can't be modified, you should look to Object.freeze()
:
const gasolineCar = { make: "Toyota", model: "Camry", type: "gasoline",};
Object.freeze(gasolineCar);
gasolineCar.model = "Echo";gasolineCar.year = 2010;console.log(gasolineCar);/* * expected result: * { * make: "Toyota", * model: "Camry", * type: "gasoline", * } */
Note that this is a shallow freeze, and nested object properties can still be modified. You'd need to look to something more advanced for a deep freeze, but exercise caution in doing so as you might freeze up your entire program!
Alternatively, if you want to make sure new properties aren't added, but you still want to be able to modify the values of the properties that exist, you can reach for Object.seal()
:
const gasolineCar = { make: "Toyota", model: "Camry", type: "gasoline",};
Object.seal(gasolineCar);
gasolineCar.model = "Echo";gasolineCar.year = 2010;console.log(gasolineCar);/* * expected result: * { * make: "Toyota", * model: "Echo", * type: "gasoline", * } */
const
Variables and Array Elements
Similar to const
variable objects, const
variable arrays can have their element values modified and elements may be added or removed:
const ingredients = ["bread", "peanut butter", "jelly"];ingredients[2] = "jam";ingredients.push("banana");ingredients.shift();console.log(ingredients);// expected result: ["peanut butter", "jam", "banana"]
And like with objects, you can use Object.freeze()
to restrict both modification and additions/removals:
const ingredients = ["bread", "peanut butter", "jelly"];Object.freeze(ingredients);ingredients[2] = "jam";ingredients.push("banana");// TypeError: Cannot add property 3, object is not extensibleingredients.shift();// TypeError: Cannot assign to read only property "0" of objectconsole.log(ingredients);// expected result: ["bread", "peanut butter", "jelly"]
Or you can use Object.seal()
to only restrict additions and removals, but allow modification to array elements:
const ingredients = ["bread", "peanut butter", "jelly"];Object.seal(ingredients);ingredients[2] = "jam";ingredients.push("banana");// TypeError: Cannot add property 3, object is not extensibleingredients.shift();// TypeError: Cannot assign to read only property "0" of objectconsole.log(ingredients);// expected result: ["bread", "peanut butter", "jam"]
Initializing a const
Variable without a Value
For const
variables, a value must be assigned when you are creating the variable. This makes sense considering that the value of the variable can't be reassigned.
const hologramLimousine;// expected result: SyntaxError: Missing initializer in const declaration
Redeclaring a const
Variable
Finally, like with let
variables, you cannot redeclare a const
variable:
{ const greeting = "Howdy"; const greeting = "Hello"; // expected result: SyntaxError: Identifier "greeting" has already been declared}
However, like let
variables, const
variables declared in different scopes will work:
const greeting = "Howdy";
if (true) { const greeting = "Hello"; console.log(greeting); // expected result: "Hello"}
console.log(greeting);// expected result: "Howdy"
Redeclaring a const
variable with var
It should go without saying by now that redeclaring const
variable with var
is not permitted:
let greeting = "Hello";var greeting = "Howdy";// expected result: SyntaxError: Identifier "greeting" has already been declared
The opposite also doesn't work:
var greeting = "Howdy";const greeting = "Hello";// expected result: SyntaxError: Identifier "greeting" has already been declared
However, if the const
variable is within its own block...
var greeting = "Howdy";{ const greeting = "Hello"; console.log(greeting); // expected result: "Hello"}
console.log(greeting);// expected result: "Howdy"
...then we have no issues.
Summary
That was a lot, so here is a quick summary of var
, let
, and const
rules across the five behaviors we've examined:
Behavior | var | let | const |
---|---|---|---|
Scope | Globally Scoped or Function Scoped | Globally Scoped or Block Scoped | Globally Scoped or Block Scoped |
Hoist | Allowed | Disallowed | Disallowed |
Reassigning the Variable's Value | Allowed | Allowed | Disallowed |
Initializing without a Value | Allowed | Allowed | Disallowed |
Redeclare | Allowed | Disallowed | Disallowed |
Sources / Further Reading
var
on MDNlet
on MDNconst
on MDN- Var, Let, and Const – What's the Difference? by Sarah Chima Atuonwu on freeCodeCamp
- Difference between var, let, and const keyword in JavaScript by Harsh Jain on educative.io
- Should I use
window.variable
orvar
? Thread on StackOverflow - Understanding Variables, Scope, and Hoisting in JavaScript by Tania Rascia on Digital Ocean
- Do let statements create properties on the global object? Thread on StackOverflow
- ES6 In Depth:
let
andconst
by Jason Orendorff on Mozilla Hacks - Block Scoped Declarations section of You Don't Know JS: ES6 and Beyond (First Edition) by Kyle Simpson