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:

  1. To track state across the program's lifecycle
  2. 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:

  1. Oven has been turned on.
  2. Oven has been heated to 350.
  3. 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 = .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 deleted
console.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 here
greeting = "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:

  1. Scope
  2. Hoisting
  3. Reassigning the Variable's Value
  4. Initializing the Variable without a Value
  5. 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 = .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 = .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 extensible
ingredients.shift();
// TypeError: Cannot assign to read only property "0" of object
console.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 extensible
ingredients.shift();
// TypeError: Cannot assign to read only property "0" of object
console.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