This seems to be a very common misconception that just won’t die. I keep running into it in blog posts, Twitter discussions, and even books. Here’s my attempt at setting things straight.
const creates an immutable binding
ES6 const does not indicate that a value is ‘constant’ or immutable. A const value can definitely change. The following is perfectly valid ES6 code that does not throw an exception:
const foo = {};
foo.bar = 42;
console.log(foo.bar);
// → 42
The only thing that’s immutable here is the binding. const assigns a value ({}) to a variable name (foo), and guarantees that no rebinding will happen. Using an assignment operator or a unary or postfix -- or ++ operator on a const variable throws a TypeError exception:
const foo = 27;
// Any of the following uncommented lines throws an exception.
// Assignment operators:
foo = 42;
foo *= 42;
foo /= 42;
foo %= 42;
foo += 42;
foo -= 42;
foo <<= 0b101010;
foo >>= 0b101010;
foo >>>= 0b101010;
foo &= 0b101010;
foo ^= 0b101010;
foo |= 0b101010;
// Unary `--` and `++`:
--foo;
++foo;
// Postfix `--` and `++`:
foo--;
foo++;
ES6 const has nothing to do with immutability of values.
So, how to make a value immutable?
Primitive values, i.e. numbers, strings, booleans, symbols, null, or undefined, are always immutable.
var foo = 27;
foo.bar = 42;
console.log(foo.bar);
// → `undefined`
To make an object immutable, use Object.freeze(). It has been around since ES5 and is widely available nowadays.
const foo = Object.freeze({
'bar': 27
});
foo.bar = 42; // throws a TypeError exception in strict mode;
// silently fails in sloppy mode
console.log(foo.bar);
// → 27
Note that Object.freeze() is shallow: object values within a frozen object (i.e. nested objects) can still be mutated. The MDN entry on Object.freeze() provides an example deepFreeze() implementation that can be used to make objects fully immutable.
const vs. let
The only difference between const and let is that const makes the contract that no rebinding will happen.
Everything I wrote here so far are facts. What follows is entirely subjective, but bear with me.
Given the above, const makes code easier to read: within its scope, a const variable always refers to the same object. With let there is no such guarantee. As a result, it makes sense to use let and const as follows in your ES6 code:
- use
constby default - only use
letif rebinding is needed - (
varshouldn’t be used in ES6)
Do you agree? Why (not)? I’m especially interested in hearing from developers who prefer let over const (i.e. even for variables that are never rebound). If you’re using let without rebinding, why are you using let in the first place? Is it because of the “const is for constants” misunderstanding, or is there another reason? Let me know in the comments!
Comments
Gijs wrote on :
I think it’s a matter of opinion whether this:
…is easier to read / “better” than:
I certainly got used to the latter over the course of the pre-ES6 years, and don’t see the point in the extra line in the first example. But then that’s probably just me. :-)
Mathias wrote on :
Gijs: IMHO the former is much more readable, but in such a situation I tend to use the ternary operator, which avoids the issue altogether:
Anyway, I’m more interested in how people use
letvs.const. A lot of developers seem to useletby default for some reason, and I wonder if the whole “constis for constants!” misunderstanding is to blame.Gijs wrote on :
Mathias: Right, I mean, clearly the example is overly simplistic — once ternaries start going over 2-3 lines the
if…elseversion is easier to follow. The point being that IMO thevarhoisting means there is still space forvaroverletin certain situations. Usingletfor globals is also problematic because of the global lexical scope (see also https://esdiscuss.org/topic/global-lexical-tier), so that’s another reason to usevarin some circumstances.Mathias wrote on :
Gijs: I prefer declaring the variable outside of the
ifblock — it’s much more readable that way, IMHO. But hey, to each his own. :)The global
letproblem is interesting, but it shouldn’t impact how developers write their code. Creating global variables has always been a bad practice, except when exporting a library — and with ES6 modules and<script type=module>(and a transpiler until all browsers support it), that problem’s solved.MaxArt wrote on :
Keep in mind that
Object.freezedoes not “deep-freeze” an object, i.e. object values of a frozen object won’t be frozen as well.Mathias wrote on :
MaxArt: Good point! It’s explained on the MDN page I linked to, but you’re right — I should’ve mentioned this in the write-up explicitly. Fixed.
redoPop wrote on :
Kyle Simpson did a great job making the case against using
constby default then switching toletif needed:(Context, and his complete post, is here.)
So far I agree with his recommendation to use
constonly for variables you're planning to treat as non-reassignable.Patrick Mueller wrote on :
From your second example:
This code does not actually throw an exception:
…but this will:
I’ve seen some folks confused by this… including myself. I’ve finally switched over to the
'use strict'life-style.Mathias wrote on :
Patrick: That code should throw an exception, even in sloppy mode, as per the spec. Perhaps you are thinking of the legacy (non-standard)
constbehavior that used to be in Firefox and is still in stable Chromium at the moment?D wrote on :
To reproduce it in the Chrome Dev console:
Moritz wrote on :
I totally agree with the
constandletusage, but don’t understand why we shouldn’t usevaranymore?Why is
constandvarnot a thing? I was under the impression thatletbehaves the same asvar, but is block-scoped and not function-scoped.Mathias wrote on :
Moritz: Why would you still use
var(and the annoying hoisting behavior that comes with it)? There’s nothing aboutvarthatconst/letcan’t do better. If you need function scoping, create theconst/letat the top of the function.dino wrote on :
@Moritz
The only feature var has is hoisting and it's a feature most people didn't want to use before. It's confusing and all linters report it.
In the middle of const and let, a var might become something obviously used for hoisting. But it's too early.
Moritz wrote on :
Mathias: I can't think of a reason to use
varinstead ofleteither. I guess it just feels weird to abandon it entirely. I might also feel sorry for it, haha :D But there is also no downside in usingvarthough (if function scoping isn't seen as a downside)? Does code with mixedvar/letlook messy?