The Operator Tour
Mental model: An operator is a function with punctuation for a name, and precedence decides who gets the operands.
Level: beginner · about 11 minutes
console.log(7 / 2); // 3.5 there is no integer division
console.log(7 % 2); // 1
console.log(-7 % 2); // -1 the sign follows the left operand
console.log(7 % -2); // 1 the right operand's sign is ignored
console.log(2 ** 10); // 1024
console.log(2 ** -1); // 0.5Read the comments, then run it and check every line.
An operator takes one or more values (its operands) and produces a new value. That is all. The interesting part is not the list of operators, it is knowing which operator grabs which operand, and what each one does when the types are not what you expected.
Increment and decrement: where you put it matters
Postfix a++ returns the old value
let a = 5;
console.log(a++); // 5
console.log(a); // 6
Prefix ++b returns the new value
let b = 5;
console.log(++b); // 6
console.log(b); // 6Both change the variable by one. They differ only in what the expression evaluates to. When the value is unused, as in a for header, the two are interchangeable, so prefer the form that reads better and never mix them into a larger expression.
Assignment operators
let n = 10;
n += 5; // 15 shorthand for n = n + 5
n -= 3; // 12
n *= 2; // 24
n /= 4; // 6
n %= 4; // 2
n **= 3; // 8
console.log(n); // 8
Logical assignment: &&=, ||=, ??=
const settings = { volume: 0, label: '' };
settings.volume ||= 50; // 0 is falsy, so it is replaced
settings.label ??= 'none'; // '' is not nullish, so it survives
settings.mode ??= 'dark'; // undefined, so it is filled in
console.log(settings); // { volume: 50, label: '', mode: 'dark' }Three operators, three different questions about the current value.
| Operator | Assigns when the current value is | Equivalent to |
|---|---|---|
x &&= y | truthy | x && (x = y) |
x ||= y | falsy (0, '', false, null, undefined, NaN) | x || (x = y) |
x ??= y | nullish only (null or undefined) | x ?? (x = y) |
Precedence and associativity
Precedence decides which operator binds tighter. Associativity decides what happens between two operators of equal precedence. You only need the rows beginners actually hit.
| Level | Operators | Associativity |
|---|---|---|
| highest | ( ) grouping | n/a |
a.b a[b] f() new X() | left to right | |
a++ a-- (postfix) | n/a | |
! -a +a typeof void ++a --a | right to left | |
** | right to left | |
* / % | left to right | |
+ - | left to right | |
< <= > >= instanceof in | left to right | |
=== !== == != | left to right | |
&& | left to right | |
|| and separately ?? | left to right | |
cond ? a : b | right to left | |
= += &&= ||= ??= | right to left | |
| lowest | , comma | left to right |
console.log(2 + 3 * 4); // 14, not 20
console.log(2 ** 3 ** 2); // 512, because ** is right associative
console.log((2 ** 3) ** 2); // 64
console.log(true || false && false); // true, && binds tighter than ||
console.log(-(2 ** 2)); // -4, parens required
Two operators you mostly read, rarely write
const x = (1, 2, 3);
console.log(x); // 3, the comma operator evaluates all and yields the last
console.log(void 0); // undefined, void discards its operand
console.log(typeof void 'anything'); // 'undefined'
for (let i = 0, j = 3; i < j; i++, j--) console.log(i, j); // 0 3 then 1 2
comma operator- evaluates left to right, returns the last value. Common in minified code and
forheaders. `void 0`- a guaranteed
undefined. It predatesundefinedbeing safe from reassignment. `typeof`- a unary operator, not a function.
typeof(x)just adds pointless parens.
let i = 1;
const result = i++ + ++i;
console.log(result, i);i++ yields 1 and leaves i at 2. Then ++i raises i to 3 and yields 3. So result is 1 + 3, which is 4, and i is 3. This is also a good argument for never putting ++ inside a larger expression.
Try it yourself
Operator bench
const rows = [
['7 / 2', 7 / 2],
['7 % 2', 7 % 2],
['-7 % 2', -7 % 2],
['2 ** 3 ** 2', 2 ** 3 ** 2],
['5 / 0', 5 / 0],
['0.1 + 0.2', 0.1 + 0.2],
];
for (const [label, value] of rows) console.log(label.padEnd(12), value);
Add rows of your own. Try 5 / 0, 0 / 0, 2 ** 53 + 1, and 0.1 + 0.2.
Exercises
A modulo that never goes negative
Write wrapIndex(index, length) that returns a position always inside 0 to length - 1, wrapping around in both directions. wrapIndex(-1, 3) is 2, wrapIndex(4, 3) is 1. Use the remainder operator, and remember its sign follows the left operand.
Check yourself
- What does this log?
- 1 -1 —
%returns a remainder whose sign matches the left operand, so-10 % 3is-1. A true modulo would give2. Use((n % m) + m) % mwhen you need the always-positive version. - What is the value of
2 ** 3 ** 2? - 512 —
**is right associative, so it groups as2 ** (3 ** 2), which is2 ** 9, which is512. Almost every other binary operator groups left to right, which is why this one catches people. config.retriesis0. Afterconfig.retries ||= 3, what is it?3—||=assigns whenever the current value is falsy, and0is falsy, so a perfectly valid setting of zero retries gets overwritten.??=only assigns fornullandundefined, which is what you almost always want for configuration.
Common mistakes
- Expecting
%to behave like a mathematical modulo for negative numbers. - Using
||=or||for defaults when0,''orfalseare real values. - Burying
++inside a bigger expression, then debugging the reading order instead of the logic.
Takeaways
%is a remainder: its sign follows the left operand.**is the only common operator that associates right to left.&&=,||=and??=differ only in the question they ask about the current value.- When precedence is not obvious to a reader, add parentheses instead of a comment.