New: Logical Assignment Operators
Since I finished the book, a new proposal has reached Stage 3 4 (as of the July 2020 meeting): Logical Assignment Operators. It adds three new compound assignment operators to the language: ||=
, &&=
, and ??=
. At first glance it seems like these would be just like the existing compound assignment operators (+=
, -=
, and such), but they have a twist: They short-circuit (even the assignment part).
First let's look at basic operation, then we'll come back to short-circuiting.
Basic Operation
Like their mathematical cousins, the new operators combine an operation with an assignment. ||=
(the proposal calls it "Or Or Equals") combines ||
with assigning back to the left-hand side:
let left = false;
let right = true;
left ||= right;
console.log(left); // true
Similarly, "And And Equals" does the same for &&
:
let left = true;
let right = false;
left &&= right
console.log(left); // false
And "QQ Equals" (as the proposal calls it; I prefer "Nullish Equals") does it for nullish coalescing (see Chapter 17 of the book):
let left = undefined;
let right = 42;
left ??= right;
console.log(left); // 42
So far, so simple. Now the twist: Depending on the value of the left-hand side, the assignment part of that may not happen at all thanks to short-circuiting.
Short Circuiting
You may know that the logical operators short-circuit their operation. For instance, left || right
doesn't evaluate right
at all if left
is already truthy; there's no need to evaluate the right-hand side, the operation will result in left
's value. If the right-hand operand involved side-effects (perhaps it's a function call), the fact it isn't evaluated is important.
The logical assignment operators short-circuit too, but they don't just skip evaluating right
's value, they also skip the assignment back to left
! After all, there's no point in writing left
's own value back to it. You can see that in practice if you're using an accessor property:
const obj = {
_left: true,
get left() {
const result = this._left;
console.log(`Getting left: ${result}`);
return result;
},
set left(value) {
console.log(`Setting left: ${value}`);
this._left = value;
}
}
// Using a simple assignment and `||`:
obj.left = obj.left || false;
// =>
// Getting left: true
// Setting left: true
// Using compound assignment:
obj.left ||= false;
// =>
// Getting left: true
Notice there's no "Setting" log line; that's because the setter wasn't called, because the operation short-circuited.
Details in the proposal of course, but here's the fundamental logic (slightly simplified) for left op= right
where op
is ||
, &&
, or ??
:
- Get the Reference for
left
and call it lref. (A Reference is basically a spec mechanism for identifying where a value came from, such as a variable, parameter, property, etc.) - Get the value of lref and remember it as lvalue
- Perform the operation that
op
would perform on lvalue to determine whether to short-circuit; if short-circuiting, return lvalue and skip the remaining steps - Get the Reference for
right
, get its value, and remember the value as rvalue - Store rvalue in lref
- Return rvalue
For ||=
Step 3 is:
- Convert lvalue to boolean; if the result is
true
, return lvalue and skip the remaining steps
For &&=
it's the same but with false
:
- Convert lvalue to boolean; if the result is
false
, return lvalue and skip the remaining steps
For ??=
it's just a straight check:
- If lvalue is neither
undefined
nornull
, return lvalue and skip the remaining steps
There were arguments on both sides of whether the assignment part should be short-circuited. Concerns about short-circuiting came from at least two perspectives:
- It makes these operators different from the mathematical compound assignment operators, which never short-circuit;
left += 0
always writes back toleft
, which is something you can observe if it's an accessor property. - It makes the naïve explanation "
left ||= right
is basicallyleft = left || right
" incorrect, sinceleft = left || right
will always write back toleft
butleft ||= right
may not; which, again, is observable (for instance, ifleft
is an accessor).
Countering those concerns:
- The
||
,&&
, and??
operators are already different from mathematical operators.||
,&&
, and??
short-circuit; mathematical operators don't. Since they're already different, it's reasonable that the compound assignment versions of them are different as well. - The naïve explanation is already incorrect.
left += right
is observably different fromleft = left + right
even though it always writes back toleft
: In the compound form,left
is only evaluated once, but in the simple form,left
is evaluated twice. That's observable ifleft
isn't just a simple variable or property reference, but something with a side-effect, likecounter[index++] += 42
, which is quite a bit different fromcounter[index++] = counter[index++] + 42
. - Ruby, C#, and CoffeeScript all have these operators, and all short-circuit them. Ruby's have had this behavior for well over a decade. Making JavaScript different in this regard would break people's intuition coming from other languages with similar operators.
Destructuring...?
You may be wondering, as I was, whether this is valid:
// Is this valid...?
({ left } ??= obj); // Meaning something like `left = left ?? obj.left`
No, it isn't. Which is consistent with how the mathematical compound operators are handled. For now, the assignment target must be simple, not destructuring.
I don't know why I never thought to wonder about that with mathematical compound assignments, but I never did; and yet, I immediately wondered about it with these logical ones, particularly ??=
. That said, the cognitive load of understanding that destructuring nullish coalescing logical assignment is...well...pretty high. :-)
What I didn't think to ask, though, was a question asked by others in a couple of proposal issues, such as #7: Can you use ??=
to provide a default value for a destructuring target when the source value is null
(rather than only when it's undefined
), like this?
// Can you do this...?
const obj = { value: null };
const {value ??= "default"} = obj;
console.log(value); // "default"
The answer is no, you can't, but not because people necessarily thought it was a bad idea; it's just that destructuring patterns are a completely different beast from compound assignment operators, so it was out of scope for this proposal. It could be the subject of a future proposal, though...
Wrapup
I'm really glad to see these new operators, and glad the semantics ended up the way they did. I'll definitely be using them.
Happy coding!
Have a question or comment about this post? Ping me on Mastodon at @tjcrowdertech@hachyderm.io!