Part 6 — Operators
Arithmetic, Assignment, Comparison, Logical & short-circuiting, Nullish coalescing ??
Arithmetic#
+ - * / % ** ++ --. Notes: ** is exponentiation (2 ** 10 === 1024); % is remainder, not modulo, so it keeps the sign of the dividend (-5 % 3 === -2).
Assignment#
= += -= *= /= %= **= &&= ||= ??= &= |= ^= <<= >>= >>>=
Logical assignment (ES2021) 🟢:
a ||= b; // a = a || b (assign if a is falsy)
a &&= b; // a = a && b (assign if a is truthy)
a ??= b; // a = a ?? b (assign only if a is null/undefined)
Comparison#
| Operator | Meaning | Coerces? |
|---|---|---|
== |
loose equality | yes ⚠️ |
=== |
strict equality | no 🟢 |
!= / !== |
inequalities | as above |
< > <= >= |
ordering | yes (to number, or string compare) |
🟢 Always use ===/!==. The only common exception is x == null, which neatly checks "null or undefined" in one go.
0 == "0" // true ⚠️ (coercion)
0 == "" // true ⚠️
0 == false // true ⚠️
null == undefined // true (special-cased)
null == 0 // false ⚠️ (null only loosely equals undefined)
NaN === NaN // false — use Number.isNaN() or Object.is()
Logical & short-circuiting#
a && b // if a falsy → a, else b
a || b // if a truthy → a, else b
!a // boolean negation
Short-circuiting: && stops at the first falsy, || at the first truthy — the right side may never run. Used for guards and defaults:
user && user.save(); // call only if user exists (pre-optional-chaining)
const name = input || "Guest"; // ⚠️ falls back for "", 0, false too
Nullish coalescing ??#
Returns the right side only when the left is null or undefined — unlike ||, it respects 0, "", and false:
const count = userCount ?? 10; // 0 stays 0
const label = title || "N/A"; // "" becomes "N/A" (often a bug!)
⚠️ You can't mix ?? with ||/&& without parentheses: a ?? b || c is a SyntaxError.
Optional chaining ?.#
Short-circuits to undefined if the thing before it is null/undefined:
user?.address?.city // undefined instead of throwing
user?.getName?.() // call only if it exists
arr?.[0] // safe indexing
⚠️ ?. only guards the access immediately after it. a?.b.c still throws if a.b is null. And ?. does not protect assignments.
Spread ... and rest ...#
Same token, opposite jobs:
// Spread — expands
const merged = { ...a, ...b }; // later keys win
const clone = [...arr]; // shallow copy
Math.max(...[3, 1, 2]); // 3
// Rest — collects
function sum(...nums) { return nums.reduce((a, b) => a + b, 0); }
const [first, ...others] = [1, 2, 3]; // others = [2, 3]
const { id, ...restProps } = obj;
Bitwise#
& | ^ ~ << >> >>>. Operate on 32-bit integers. Handy tricks: x | 0 and ~~x truncate to int (🔴 prefer Math.trunc), n & 1 tests oddness, a ^ b ^ a === b. >>> is the unsigned right shift.
delete, void, in, instanceof#
const o = { a: 1 };
delete o.a; // removes property → true; returns true even if absent
void 0; // evaluates operand, returns undefined
"a" in o; // does key exist (including inherited)? → false now
[] instanceof Array; // prototype-chain check
⚠️ delete on array elements leaves a hole (empty slot), it does not reindex — use splice to remove array items.
Ternary and comma#
const r = cond ? "yes" : "no";
let x = (1, 2, 3); // comma evaluates all, returns last → 3 (rarely useful)
Operator precedence & coercion#
Full precedence is a table you rarely memorize — when in doubt, add parentheses. The high-value facts: member access ./?. and () bind tightest; unary before binary; ** is right-associative (2 ** 3 ** 2 === 512); assignment is lowest and right-associative; ?? cannot mix with ||/&&.
Interview questions (Part 6):
- Difference between
||and??? Give a bug||causes that??fixes. - Why is
NaN === NaNfalse, and how do you test forNaN? - What does optional chaining protect and what does it not protect?
- Predict:
2 ** 3 ** 2. Explain associativity.