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 Q&A#
Q1. Difference between || and ??? Give a bug || causes that ?? fixes.
|| falls back on any falsy value; ?? falls back only on null/undefined. So count || 10 wrongly replaces a legitimate 0 with 10, whereas count ?? 10 keeps 0. Same trap for "" and false.
Q2. Why is NaN === NaN false, and how do you test for NaN?
Per IEEE-754, NaN is defined as unequal to everything including itself. Test with Number.isNaN(x) (no coercion) or Object.is(x, NaN).
Q3. What does optional chaining protect and what does it not protect?
?. short-circuits to undefined only for the access immediately after it when the left side is null/undefined. It does not protect subsequent accesses (a?.b.c still throws if a.b is null) and does not guard assignments.
Q4. Predict: 2 ** 3 ** 2. Explain associativity.
512. ** is right-associative, so it evaluates as 2 ** (3 ** 2) = 2 ** 9 = 512, not (2 ** 3) ** 2 = 64.