Operators
JavaScript’s operator surface is C-family at the core, with substantial additions: strict equality (===/!== — distinct from coercive ==/!=), nullish coalescing (?? — admit “use this if null/undefined”), optional chaining (?. — admit “access if not nullish”), spread/rest (... — admit substantial unpacking), exponentiation (**), logical assignment (&&=, ||=, ??=). The conventional contemporary discipline: always ===, never == (except == null); ?? over || for defaults; ?. for substantial nil-safe access. The combination — C-family arithmetic and comparison, the strict equality discipline, the optional/nullish chain, the substantial spread mechanism — is the substance of JavaScript’s expression surface.
Arithmetic
a + b // addition (and string concat!)
a - b // subtraction
a * b // multiplication
a / b // division (always float; no integer division)
a % b // remainder
a ** b // exponentiation (ES2016)
-a // unary negation
+a // unary plus (numeric coercion)
++a // pre-increment
a++ // post-increment
--a // pre-decrement
a-- // post-decrement
The principal pitfall: + is both numeric addition and string concatenation:
1 + 2; // 3 (numeric)
"1" + 2; // "12" (concat)
"1" - 2; // -1 (numeric; - has no string op)
[] + []; // "" (both coerce to empty string)
[] + {}; // "[object Object]"
{} + []; // 0 (block + array)
The conventional defence: explicit conversion via Number(), String():
Number(input) + 5; // explicit numeric
`${input} suffix`; // explicit string (template literal)
For BigInt, the operators must use BigInt operands:
1n + 2n; // 3n
1n + 2; // TypeError
1n + BigInt(2); // 3n
Comparison
a == b // loose equality (coerces) — AVOID
a != b // loose inequality — AVOID
a === b // strict equality (no coercion)
a !== b // strict inequality
a < b
a > b
a <= b
a >= b
The principal pitfalls of loose equality:
1 == "1"; // true (coerced)
1 == true; // true (coerced)
0 == false; // true
null == undefined; // true (both nullish)
"" == 0; // true (substantial weirdness)
[] == false; // true
[1] == 1; // true
" " == 0; // true (" " → 0 → false → 0)
// vs strict:
1 === "1"; // false
1 === true; // false
null === undefined; // false
The conventional contemporary discipline: always use === and !==. The exception:
if (value == null) { ... } // matches both null and undefined
Logical operators
a && b // AND (short-circuit, returns operand)
a || b // OR (short-circuit, returns operand)
!a // NOT
a ?? b // nullish coalescing (ES2020)
The && and || return one of the operands, not just true/false:
true && "yes"; // "yes"
false && "yes"; // false
"a" && "b"; // "b"
null || "default"; // "default"
0 || "default"; // "default" (0 is falsy)
"" || "default"; // "default"
"value" || "default"; // "value"
The ?? returns the right only if the left is null or undefined:
0 ?? "default"; // 0 (0 is not nullish)
"" ?? "default"; // "" (empty string is not nullish)
null ?? "default"; // "default"
undefined ?? "default"; // "default"
The conventional contemporary discipline:
||— substantial “any falsy → default” patterns.??— substantial “null/undefined → default” patterns; admits0,"",falseas valid.
const port = config.port ?? 8080; // admits port=0
const port = config.port || 8080; // wrong if port=0 is valid
const name = user.name ?? "anonymous"; // admits name=""
Bitwise operators
a & b // AND
a | b // OR
a ^ b // XOR
~a // NOT
a << b // left shift
a >> b // signed right shift
a >>> b // unsigned right shift (JS-specific)
JavaScript bitwise operators coerce to 32-bit signed integers — admit substantial pitfalls for large numbers:
2 ** 32 | 0; // 0 (overflow)
0xFFFFFFFF | 0; // -1 (signed)
// Common bit-twiddling:
n | 0; // truncate to 32-bit int (faster than Math.floor for some cases)
n << 0; // same
~~n; // double-NOT (truncate)
For BigInt, bitwise operators admit arbitrary precision:
0xFFFFFFFFn | 0n; // 0xFFFFFFFFn (no overflow)
Assignment operators
a = b // basic
a += b // a = a + b
a -= b
a *= b
a /= b
a %= b
a **= b
a &= b // bitwise
a |= b
a ^= b
a <<= b
a >>= b
a >>>= b
// Logical assignment (ES2021):
a &&= b // a = a && b
a ||= b // a = a || b
a ??= b // a = a ?? b
The logical-assignment forms admit substantial patterns:
config.timeout ??= 30; // set if null/undefined
items[key] ||= []; // set if any falsy
flag &&= condition; // toggle off
Spread ...
The ... admits spread in calls, literals, destructuring:
// In array literal:
const a = [1, 2, 3];
const b = [...a, 4, 5]; // [1, 2, 3, 4, 5]
// In object literal:
const obj1 = { x: 1, y: 2 };
const obj2 = { ...obj1, z: 3 }; // { x: 1, y: 2, z: 3 }
// In function call:
console.log(...["a", "b", "c"]); // a b c
Math.max(...[3, 1, 4]); // 4
// In destructuring (rest):
const [first, ...rest] = [1, 2, 3, 4]; // first=1, rest=[2,3,4]
const { name, ...others } = { name: "Alice", age: 30, role: "admin" };
// name="Alice", others={age:30, role:"admin"}
// In function parameters (rest):
function sum(...nums) {
return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3); // 6
Optional chaining ?.
The ?. admits “access this if not nullish”:
const user = getUser();
const name = user?.name; // undefined if user is null/undefined
const street = user?.address?.street; // chained
const first = list?.[0]; // bracket access
const result = fn?.(arg); // function call
// Without ?. (substantial verbosity):
const street = user && user.address && user.address.street;
The form short-circuits to undefined if any link is nullish.
For delete and call:
delete obj?.prop; // no-op if obj is nullish
obj.method?.(); // call only if method exists
optionalCallback?.(arg); // call if callback is set
The conventional contemporary discipline: combine with ?? for safe-access-with-default:
const name = user?.name ?? "anonymous";
const port = config?.server?.port ?? 8080;
Ternary ?:
const max = a > b ? a : b;
const status = active ? "running" : "stopped";
// Nested (avoid — substantial readability burden):
const sign = n > 0 ? "positive" : n < 0 ? "negative" : "zero";
Comma operator
const x = (a = 1, b = 2, a + b); // x = 3 (last expression)
// In for loops:
for (let i = 0, j = 10; i < j; i++, j--) { ... }
The comma admits multiple expressions in places that admit one — substantial for for loop initialisation/update.
typeof, instanceof, in
Treated more substantially in Types.
typeof value; // string describing type
value instanceof Class; // prototype chain check
"key" in obj; // own or inherited property
delete
delete obj.prop; // remove property; returns true
delete obj["key"];
// On arrays:
const arr = [1, 2, 3];
delete arr[1]; // arr = [1, undefined, 3]
// length unchanged; "hole" created
// Conventional alternatives for array removal:
arr.splice(1, 1); // mutate; remove at index
const filtered = arr.filter((_, i) => i !== 1); // new array
The conventional discipline avoids delete on arrays — admit substantial sparse-array pitfalls.
void
The void operator evaluates its operand and returns undefined:
void 0; // undefined
void compute(); // discard the result
// Common use: signal "discard this value":
button.onclick = () => void asyncTask(); // explicit "ignore the promise"
new, this, super
const date = new Date(); // construct
const obj = new MyClass(args);
// Inside class:
class Child extends Parent {
constructor() {
super(); // call parent constructor
this.name = "child";
}
method() {
super.method(); // call parent method
}
}
Operator precedence
The principal precedence (high to low):
| Operators |
|---|
(), [], ., ?. |
new (with args) |
!, ~, +a, -a, ++, --, typeof, void, delete |
** (right-associative) |
*, /, % |
+, - |
<<, >>, >>> |
<, <=, >, >=, in, instanceof |
==, !=, ===, !== |
& |
^ |
| |
&& |
||, ?? |
?: |
=, +=, etc. |
, (lowest) |
The conventional discipline uses parentheses for clarity in non-trivial expressions.
&& and ?? cannot mix
a && b ?? c; // SYNTAX ERROR
(a && b) ?? c; // OK
a && (b ?? c); // OK
a || b ?? c; // SYNTAX ERROR
(a || b) ?? c; // OK
The mechanism admits substantial precedence-related safety.
Common patterns
Default value
const port = config.port ?? 8080;
const name = user.name ?? "anonymous";
const items = list ?? [];
Optional chain
const city = user?.address?.city;
const first = arr?.[0];
const result = fn?.(arg);
Spread for immutable update
function updateUser(user, changes) {
return { ...user, ...changes };
}
function addItem(items, newItem) {
return [...items, newItem];
}
function removeItem(items, idx) {
return items.filter((_, i) => i !== idx);
}
Spread for max/min
Math.max(...arr);
Math.min(...arr);
For substantial arrays, the spread admits substantial limits:
// Stack overflow for very large arrays:
const max = Math.max(...veryLargeArray);
// Conventional defence:
const max = veryLargeArray.reduce((a, b) => Math.max(a, b), -Infinity);
Destructuring with default
const { name = "anonymous", age = 0 } = user ?? {};
Conditional spread
const obj = {
a: 1,
...(condition && { b: 2 }),
...(otherCondition ? { c: 3 } : {}),
};
The pattern admits substantial conditional inclusion.
Function-call with default
function fetch(url, options = {}) {
const { method = "GET", headers = {}, body = null } = options;
// ...
}
Logical assignment for memoisation
function getData(key) {
cache[key] ??= computeExpensive(key);
return cache[key];
}
Truthiness with &&
const items = data && data.items; // null if data is null
const items = data?.items; // modern equivalent
// Conditional render (React-style):
{user && <Welcome name={user.name} />}
{user ? <Welcome /> : <SignIn />}
Falsy filter
const truthy = items.filter(Boolean); // remove null/undefined/0/""/false
const numbers = ["1", "two", "3"].map(Number).filter(Number.isFinite);
Comma for swap (rare)
[a, b] = [b, a]; // destructure-based swap (preferred)
Bitwise tricks
n | 0; // truncate to 32-bit int
~n + 1; // negate (two's complement)
n & 1; // odd if 1, even if 0
n & (n - 1) === 0; // power of 2
// Flag combination:
const READ = 1, WRITE = 2, EXECUTE = 4;
const perms = READ | WRITE; // 3
const hasRead = (perms & READ) !== 0; // true
const removed = perms & ~READ; // remove READ
Optional method invocation
listener?.dispose?.();
emitter?.emit?.("event", data);
Chained nullish coalescing
const value = first ?? second ?? third ?? "default";
Conditional property
const obj = {
name: "Alice",
...(includeEmail && { email: "alice@b.c" }),
};
A note on == exceptions
The == is admitted for one specific case:
if (value == null) { ... } // matches null AND undefined
The pattern admits substantial conciseness — equivalent to value === null || value === undefined.
For all other comparisons, use === / !==.
A note on the conventional discipline
The contemporary JavaScript operator advice:
- Use
===and!==— never==(except== null). - Use
??over||for default values when0,"",falseare valid. - Use
?.for nil-safe property access. - Use spread
...for immutable updates and unpacking. - Use logical assignment (
??=,||=,&&=) for substantial conciseness. - Use template literals over
+for string concatenation. - Use parentheses for clarity in mixed-precedence expressions.
- Avoid
deleteon array elements — usespliceorfilter. - Avoid bitwise on numbers > 2^32 — coercion to 32-bit produces unexpected results.
- Use BigInt for substantial integer arithmetic.
The combination — C-family arithmetic and comparison, strict-equality discipline, the substantial nullish/optional chain (??, ?.), the spread/rest mechanism, the logical-assignment shortcuts, the operator precedence and parenthesisation discipline — is the substance of JavaScript’s expression surface. The discipline produces concise, type-safe-where-possible expressions; the conventional pitfalls (loose equality, type coercion) admit substantial defence through the strict operators and explicit conversions.