Daniel Immke

Behind the syntax: let and const variables

There is a lot more going under the hood than simple syntax changes.

Back in 2015, the JavaScript language got it’s first significant update in 6 years. ES2015 – more colloquially known as ES6 – was released and started making its way into people’s workflows. With it came a type of article on JavaScript that I have come to really dislike: The “Here’s what is new in ES6!” primer.

They usually cover the very visible changes in the language like the new variable declaration keywords. I have found that these articles tend to cover the same ground in an extremely shallow way that does a disservice to the changes in the language and to the reader. They often present these new features as simple syntax changes and don’t mention the underlying changes in functionality going on.

That’s not to say the stuff I cover in these posts isn’t discussed or written about extensively, they are. I just wanted to write something specifically for people who might be coming from one of these very basic ES6 primer articles.

What they tell you: let and const

Now in JavaScript, you declare your variables with the let and const keywords. You use let for variables you know the value is going to change and const for ones that won’t.

Amazingly, this is the level of detail I’ve seen a fair amount of articles give on the topic. But there is more going on under the hood.

What they don’t: Immutability, block scoping and hoisting

The biggest practical tip I can give about these new variable declarations is that you will not need to use let very often. Outside of using counter variables or in rare cases working with primitive data types that need to change you will almost always be using const.

The reason for this is because with const, only primitives are immutable. If your const variable is an object (or array, which is a type of object), you can add and remove properties as you please. This blog post has more on the subject if you’re into the nitty gritty details.

Secondly, these new declarations are block scoped instead of function scoped. This can seem like a small change, but it’s actually really important to understand. Having a solid understanding of how scope works in JS is very important, but I will leave that to other articles. When a variable is declared with var, it’s scope is the function it currently resides in (or global, if it isn’t inside any function.) with the new keywords, putting them inside a block gives them scope relative to that block. I’ve attached an example to illustrate.

const newFunction = () => {
  var oldVar = "Function Level";

  const newVar = "Function Level";

  if(1 === 1) { // Just need a conditional statement to be true for the sake of this example.
    var oldVar = "Block Level";

    const newVar = "Block Level";
  }

  console.log(newVar); // Returns "Function Level" because the second declaration of newVar is only accessible to code inside the if block statement. 

  console.log(oldVar); // Returns "Block Level" because JS reads the second variable declaration as a "re-declaration"
}

newFunction();

Something else you will notice if you run that code is that JS doesn’t have any issue with the two const declarations even though they are both string primitives. That further demonstrates that they are considered to be two different scopes by JS. If you try to redeclare const in that way in the same scope, JS throws an error.

This is important to know even if you are new to JS (and therefore don’t have any other reference point to learn from) because it’s key to understanding scope in JS, will help you debug older code and understanding the history of JavaScript can be really useful in gaining a deeper understanding of the language.

Block scoping is a useful feature that exists in a lot of other programming languages and was considered a “wish list” item by developers for a while. The creator of JS even admitted that not adding it was not a deliberate decision, but rather the result of a deadline crunch. People wrote complicated workarounds just to emulate it. In his book, JavaScript: The Good Parts (published in 2008) Douglass Crockford imagines a new, streamlined version of JS that gets rid of the “bad parts”. In this new language, he implemented block scoping.

Finally, the last major point that these new variable declarations change is hoisting. With traditional variable and function declarations, JavaScript will move the declarations to the top of the scope before executing. However, for variables it only moves the declarations up, not the initializations. So if you have the code var test = "foo"; it is only moving up var test; and giving it a value of undefined (which, despite what you’d think, is actually a value. So if you were checking if the variable existed, JS would tell you it does)

So JS will know the variable exists, but can’t detect the value until the initialization is called. It can be confusing, and if you want to learn more you can check this article out to do a deeper dive with the concept and numerous code examples.

The new variable keywords do not hoist in the same way as var. They still technically hoist (under the hood), but attempting to reference them before they are declared will cause an actual error: JS will act like they don’t exist. Here’s an example (Comment out line 2 to avoid the error stopping the full code from running.)

console.log(hoistedVar); // "undefined" - hoisted declaration only
console.log(nonHoistedConst); // Throws an actual error in JS: nonHoistedConst is not defined

var hoistedVar = "Hoisted";
const nonHoistedConst = "Definitely not hoisted";

console.log(hoistedVar); // Actually displays the value of the variable.
console.log(nonHoistedConst);

Why the change? Well, to avoid confusion. Generally, using hoisting has been considered poor form when writing code and led to countless hours of debugging for people who did not understand this concept. This change is simply enforcing what already was good practice when writing code.

There is also a way to write functions that avoids hoisting and is the generally accepted way to do it by most style guides. Using a function expression instead of a declaration. Here’s a simple code example that demonstrates these concepts for both variables and functions.

hoistedFunction();

// This declaration is moved to the top before the JS is executed.
function hoistedFunction() {
  console.log('Hoisted function. Bad!');
}


// If we tried to call this function before this expression, JS would throw an error!
const nonHoistedFunction = function() {
  console.log('Not hoisted! Good!')
}

nonHoistedFunction();

There is a newer ES6 syntax for creating functions: arrow functions. Those bring along their own changes and are going to be the subject of the second post in this series.

I hope that a little bit of a deeper dive helped give some further context behind these changes. Writing these posts helps me go back over and clear up any lingering misunderstandings myself. I’m looking forward to writing part 2!

Hey — My name is Daniel Immke. I’m a designer & developer currently building Pickwick.

If you liked my writing, check out more posts or subscribe.