One of the new features in ES6 that has both garnered attention and caused a fair bit of confusion is the new const declaration keyword. I’m going to briefly wade through the morass to help you make sense of this feature.

Like its cousin let, the const keyword declares variables that are scoped to the nearest enclosing { } block, as opposed to var which attaches its variable declarations to the nearest enclosing function scope.

But unlike let, const purports to declare constant variables. So what exactly does that mean and not mean?

What is a constant?

This may sound like a silly question to ask, kind of like asking, “what is a number?” But it turns out there’s more nuance to it than you might assume. It bears closer inspection.

Most simply put, a constant is a variable whose value is set at declaration time and does not change for the life of that variable. But even in that simple definition, the confusion begins. Are we talking about a value not changing, or the assignment of the value not changing?

A constant is a variable that cannot be reassigned. But that says absolutely nothing about the nature of the value it is assigned.

Read that paragraph again.

Constant Value

It seems from anecdotal observation that many JS developers have a somewhat looser definition of what a constant is. Most seem to suggest a definition like, “a value that doesn’t change.”

While that looseness may not seem like a concern, it actually is part of why const is confusing to so many developers.

Let’s observe the behavior of this code:

var x = 10;

x += 32;

x; // 42

Did the += operator change the value from 10 to 42? No.

More accurately speaking, the program created a new value 42 from the addition of 10 and 32, and this 42 was assigned to the x variable. In other words, 10 is already a value that cannot change, itself. Primitive values are unchangeable. Sometimes, the word immutable is used to refer to such values.

Let’s illustrate that point with another kind of primitive: string.

var str = "hello!";

str[4];         // "o"
str[4] = "O";   // looks like a mutation
str;            // "hello!" <-- no change!

What happened here? The value currently in str ("hello!") is a primitive string value, and it is thus immutable. The attempt to change one of its characters is futile. Compare that previous snippet to this one:

var arr = "hello!".split("");

arr[4];         // "o"
arr[4] = "O";   // looks like a mutation
arr;            // ["h","e","l","l","O","!"]
                // look, it changed! ^

The string method split(..) produces an array, which is not a primitive. Arrays are in fact mutable, which is how we’re able to change the character to "O".

There is a way to make an array immutable, if you wanted:

var arr = "hello!".split("");

Object.freeze(arr);

arr[4] = "O";
arr;            // ["h","e","l","l","o","!"]
                // no change!

And if you had run that code in strict mode, the ill-fated arr[4] = "O" statement would have thrown an error about failing to set a read-only property.

So, back to that loose definition of constant. If you hold the view that a constant is an unchanging value, then primitives are already constants regardless of what keyword you use! Moreover, you could make an array an unchanging value by using Object.freeze(..) as shown above.

But that view misses the whole point, because const really has nothing to do with the value itself.

Conventional Constant

Prior to ES6, most developers followed the convention that if they had a variable to assign a value to that shouldn’t change, they simply named the variable in all capital letters, with snake-case:

var TAX_RATE = 0.08;

function calculateSale(qty,price) {
    return (1 + TAX_RATE) * qty * price;
}

But what is the reason for indicating that TAX_RATE is a constant with this capitalization convention? What benefit does this convention really bring to the code? Would naming it tax_rate (or taxRate!) really have made the code any harder to understand?

Let me illustrate the question this way:

var TAX_RATE = 0.08;

function calculateSale(qty,price) {
    return (1 + TAX_RATE) * qty * price;
}

// a whole bunch of code

How confident are you from this code that TAX_RATE never gets accidentally or intentionally changed later in the code before calculateSale(..) is called?

The point I’m trying to make is, the convention of capitalized constant name really only conveys a suggestion that the value not be changed, and that really only appeals to us on an emotional level.

You still have to read all the code if you need to be sure that the value never changes.

const Enforcement

The relieved welcome many developers feel at finally having const seems to come mostly from const enforcing what the capitalization style always suggested: don’t change my variable!

const TAX_RATE = 0.08;

function calculateSale(qty,price) {
    return (1 + TAX_RATE) * qty * price;
}

// sneaky...
TAX_RATE = 0.07;    // nope, error!

Since we know a const can’t be reassigned, we don’t have to worry about that sneaky line, since it would throw us an error if anyone ever tried to do it. Great, problem solved, right!?

Actually, many developers assert this protection keeps you from having some unsuspecting developer accidentally change a const. Except, in reality, I think the likelihood is that a developer who needs to change a variable and gets a const-thrown error about it will probably just change the const to let and go on about their business.

It’s quite a shaky sense of security that const will prevent such changes.

Not so immutable

Moreover, here’s the rub: const only controls the assignment of a value to a variable. It has nothing to say about the value itself.

If the value happens to be a primitive, you’ve already got immutability built-in, as we saw earlier. But if you don’t have a primitive, you don’t automatically get immutability. Consider:

const LUCKY_NUMBERS = [12, 19, 42];

function playTheLotto() {
    play( LUCKY_NUMBERS );
}

// other code

What does this code look like to you? To most developers, it means that your LUCKY_NUMBERS will always be 12, 19, and 42. But… nope!

const LUCKY_NUMBERS = [12, 19, 42];

// other code

// sneaky...
LUCKY_NUMBERS[1]++;     // (evil chuckle)

Sigh.

It’s true that LUCKY_NUMBERS is always and forever going to point at that initial array, but that’s never going to make any assertions whatsoever about the contents of that array — they can easily be changed.

What exactly makes you feel better about this code in knowing that LUCKY_NUMBERS will never point at a different array/object/value?

Stop to ponder that for a moment. In what kinds of circumstances is your real concern going to be where the reference to LUCKY_NUMBERS points versus being concerned with the substance of the value itself?

Isn’t const here just creating extra noise that could, possibly, distract you from the fact that this value, this array, is not immutable, whereas your TAX_RATE value of 0.08 fundamentally is? Isn’t const just giving you a false sense of security?

In that snippet, all const does is ensure to us that no other code will try to point LUCKY_NUMBERS at a whole different value. So…

// can't do this:
LUCKY_NUMBERS = [];

// but can do this:
LUCKY_NUMBERS.length = 0;

What happens when your luck runs out?

My point is, you’ll still have to read the rest of the program to make sure no code changes the contents of LUCKY_NUMBERS.

This is especially true since all non-primitive values (objects, arrays, functions, etc) are passed by reference-copy to functions, which means not only can the contents of LUCKY_NUMBERS be changed by any code in its scope, but also by any code you pass a copy of the LUCKY_NUMBERS reference.

Immuting

So how about making it immutable?

const LUCKY_NUMBERS = Object.freeze(
    [12, 19, 42]
);

OK, great. Now the array really is immutable. Perfect.

So exactly what assistance is const providing to you in understanding what you can expect from this program?

It still only does the lesser of the two tasks: it only says that LUCKY_NUMBERS can’t be swapped out for an entirely different array/object/value. Just how likely was that to be your problem, though?

Isn’t the Object.freeze(..) immutability part way more important to the reasoning about your code than the const?

Resilience

It has been suggested that const aids readability of code solely because you can see the intent of the developer from the outset. I think I’ve kind of debunked that notion by now, or at the very least shown it’s on shaky ground.

But does const have any other intrinsic benefit to our code? Another suggestion is that the enforcement of no-reassignment means we can be sure that other parts of the code are more resilient because at least this particular variable can’t change in an unpredictable way.

Let’s try to envision such a scenario:

nonsense( [3, 5, 4, 8, 9] );
// [3, 10, 4, 8, 18]

function nonsense(arr) {

    function find(v) {
        for (var j = 0; j < len; j++) {
            if (j != i && arr[j] == v) return j;
        }

        return -1;
    }

    var len = arr.length;

    for (var i = 0; i < len; i++) {
        if (arr[i] % 2 == 0) {
            var idx = find( arr[i] + 1 );
            if (idx != -1) {
                arr[idx] *= 2;
            }
        }
    }

    return arr;
}

It doesn’t really matter what this nonsense(..) function does. I just made it up. So don’t get hung up on bikeshedding its implementation details or design.

Let’s only try to examine whether const would help the readability for any of the various declarations?

First, we can eliminate the i and the j indexers from const eligibility, since they have to change. Even if it made you feel better to pass i in as an argument to find(..), that wouldn’t change that both need to stay mutable.

Side Note: If you made i a let declaration, you’d have to pass it to find(..), since it wouldn’t be in the function scope for find(..) to use. This post isn’t really about the merits of let, but this is one of the places where I’m not as wild about let, since in cases like this it’s going to force me to either manually hoist lets to higher scopes, or to have to explicitly pass in that context. When it’s only one or two variables, no big deal, but when it’s many… ugh.

Constant Functions/Parameters

What about arr itself? Isn’t it kind of frustrating, if you’re a fan of const, that you can’t declare a parameter as a const? Would you instead prefer to write a line like:

const theArr = arr;

Would that help you reason that this code never changes where arr (actually theArr) references? Same question goes for the v parameter to find(..). I don’t see a very strong argument to be made for either, personally.

What about the function declarations for nonsense(..) and find(..)? Some developers dislike any form of hoisting, so they prefer to skip function declarations and instead use expression assignments:

var nonsense = function(arr) {

    var find = function(v) {
        // ..
    }

    // ..
}

Side Note: I really like using function hoisting, putting all my functions at the bottom of my file and my executable code, if any, at the top. That way I don’t have to go hunting for it. See the full nonsense(..) code example above for what I mean.

But should we make our two function declarations into const declaration assignments? I suppose you could argue that it might be slightly nicer to know for sure that nonsense(..) and find(..) are never overwritten. But really, is this a problem many of us have, that our functions are mysteriously and unpredictably getting overwritten regularly?

By contrast, I quite like the ability to have a function redefine itself opaquely. Abstracting such details is a powerful technique, IMO.

function doTask(cb) {
    ajax("http://some.url",function(resp){
        doTask = function(cb) { cb(resp); };
        cb(resp);
    });
}

Localized Constants

OK, back to the earlier nonsense(..) example, and now let’s talk about idx. Should it be a const? I bet many readers think so. It’s clear from reading the code that it in fact doesn’t change. So it seems like a good candidate.

But that’s actually just my point. If the scope that idx is being used in is so clear and tightly constrained as to make it easy to tell that it doesn’t change, would const actually add any benefit of readability? Eh.

I think you could rightly argue that it should be a let idx = .. declaration, because idx is only being used in that one block and doesn’t need to hoist to the whole function. But once you make it a let — by the way, that also gives you assurance that it’s not even possibly being affected at a distance by a call to find(..) — what additional purpose would const serve here?

Here’s my point:

const is block-scoped (just like let is), and most proper coding styles suggest that blocks should stay pretty short and sweet anyway. So, the places you’re going to use a const to make a block-scoped variable, you’re likely going to have short enough code that you can already know for sure at a quick glance is not going to mutate the assignment of that variable. So why const it?

const is like a night-light in a child’s room to keep them from being scared of monsters in the closet while going to sleep. It’s not really necessary, but it definitely lends emotional support.

But wait! There’s one last variable we haven’t talked about: len. I saved that one for last, on purpose.

len is at the top scope level of the nonsense(..) function, is shared across scopes, and is thus used in a non-local enough way that being able to know in advance that it can’t be changed might be helpful.

In fact, I think this is exactly what’s meant by assertions that const can ensure that other parts of the code are more resilient. Making len a constant will make sure that the i and j loops always predictably run to completion.

So, const len = arr.length it is!

But… what if we later change nonsense(..) so that it might remove items from arr? Well, now len will have to be changeable, so it’ll have to go back to a let or var.

Oh well. I guess we’ll just ignore that possibility for now.

Premature const

Many developers have asserted that they intend as of ES6 to follow this process for selecting var, let, and const:

  1. Start by default with const.
  2. Use let if const won’t work (variable needs reassignment).
  3. Never write another var, but leave legacy vars in place until refactoring (see 1 and 2).

That may seem reasonable. I certainly understand the thinking. But I’ll conclude this post by asserting that I think it’s going to play out in many cases that using const like this is premature optimization — of readability, not performance.

(2) very casually implies simple refactoring, as if that has basically no cost at all. I don’t think that’s fair.

The mindset I have to be in to realize that a const should no longer be a const, and to then responsibly analyze the potential risk areas I am introducing by changing const back to let / var — and quite possibly needing to refactor the surrounding code more heavily to reduce those risks! — is way more than just a casual and quick, “replace const with let“.

Instead, I think the decision steps should generally be:

  1. Use var for top-level variables that are shared across many (especially larger) scopes.
  2. Use let for localized variables in smaller scopes.
  3. Refactor let to const only after some code has been written and you’re reasonably sure that you’ve got a case where there shouldn’t be variable reassignment.

I’m sure plenty of people will disagree with (1). That’s fine I already made that argument in this post.

The main takeaway here is: (2) comes before (3), not the other way around.

Conclusively const

Don’t use const to make constant (immutable) values; use it to make non-reassignable variables. If you want to make an immutable array/object, use Object.freeze(..).

Remember: code is communication. If your use of a favorite feature can confuse you or others, find a different way to use it. And if that’s uncomfortable emotionally, just turn on your night-light.

This entry was written by getify , posted on Tuesday September 08 2015at 11:09 am , filed under JavaScript and tagged , , , , , , , . Bookmark the permalink . Post a comment below or leave a trackback: Trackback URL.

One Response to “Constantly Confusing ‘const’”

  • Jeff says:

    Seems to me that const and Object.freeze (and Object.seal, etc.) go together very well, like peanut butter and chocolate.

    Until const, it was hard to prevent uninvited (aka malicious) JavaScript from cloning a frozen object, and simply reassigning it to the same variable. Preventing assignment is the last piece of the puzzle for attaining immutability. Calling const a child’s nightlight isn’t quite fair. You might scoff at the desire for true immutability, but I’d say it’s a useful tool, especially in an age where your code is quite likely to be executed alongside other libraries, browser plugins, extensions, you name it.

    I wouldn’t advocate using const on a regular basis unless 1) you have a primitive value that you are certain will never change or 2) you need to prevent reassignment of a frozen (or sealed) object. You’re quite right; people shouldn’t start with const on every declaration. The saving grace is that if they do overuse const, the unpleasantness of often refactoring into let/var will eventually persuade them to be more moderate with const.

Leave a Reply

Consider Registering or Logging in before commenting.

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Notify me of followup comments via e-mail. You can also subscribe without commenting.