JavaScript Operators and Expressions
1. Arithmetic Operators (+, -, *, /, %, **) and Math Operations
| Operator | Name | Description | Example | Result |
|---|---|---|---|---|
| + | Addition / Unary Plus | Adds numbers or concatenates strings; converts to number | 5 + 3, +"42" |
8, 42 |
| - | Subtraction / Unary Minus | Subtracts numbers; negates value | 5 - 3, -5 |
2, -5 |
| * | Multiplication | Multiplies numbers | 5 * 3 |
15 |
| / | Division | Divides numbers (floating-point result) | 10 / 3 |
3.3333... |
| % | Remainder / Modulo | Returns remainder of division | 10 % 3 |
1 |
| ** | Exponentiation ES2016 | Raises to power | 2 ** 3 |
8 |
| ++ | Increment | Increases by 1 (prefix or postfix) | ++x, x++ |
Pre: return new; Post: return old |
| -- | Decrement | Decreases by 1 (prefix or postfix) | --x, x-- |
Pre: return new; Post: return old |
| Operation Type | Behavior | Special Cases |
|---|---|---|
| Number + Number | Numeric addition | 5 + 3 = 8 |
| String + Any | String concatenation | "5" + 3 = "53", "a" + true = "atrue" |
| Division by Zero | Returns Infinity |
5 / 0 = Infinity, -5 / 0 = -Infinity |
| 0 / 0 | Returns NaN |
Indeterminate result |
| NaN in Operations | Propagates NaN |
NaN + 5 = NaN, NaN * 2 = NaN |
| Infinity Operations | Mathematical infinity rules | Infinity + 1 = Infinity, Infinity * -1 = -Infinity |
Example: Arithmetic operators and edge cases
// Basic arithmetic
console.log(10 + 5); // 15
console.log(10 - 5); // 5
console.log(10 * 5); // 50
console.log(10 / 5); // 2
console.log(10 % 3); // 1 (remainder)
console.log(2 ** 3); // 8 (exponentiation)
// Increment/Decrement
let x = 5;
console.log(x++); // 5 (returns old value, then increments)
console.log(x); // 6
console.log(++x); // 7 (increments, then returns new value)
console.log(x--); // 7 (returns old value, then decrements)
console.log(--x); // 5 (decrements, then returns new value)
// String concatenation with +
console.log("Hello" + " " + "World"); // "Hello World"
console.log("5" + 3); // "53" (string concatenation)
console.log(5 + "3"); // "53"
console.log("5" + 3 + 2); // "532"
console.log(5 + 3 + "2"); // "82" (left to right: 5+3=8, then 8+"2"="82")
// Type coercion with other operators
console.log("10" - 5); // 5 (string to number)
console.log("10" * "2"); // 20
console.log("10" / "2"); // 5
console.log("10" % 3); // 1
// Unary operators
console.log(+"42"); // 42 (string to number)
console.log(+"abc"); // NaN
console.log(-"5"); // -5
// Special values
console.log(5 / 0); // Infinity
console.log(-5 / 0); // -Infinity
console.log(0 / 0); // NaN
console.log(NaN + 10); // NaN (NaN propagates)
console.log(Infinity - 1); // Infinity
2. Assignment Operators (=, +=, -=, *=, /=) and Compound Assignment
| Operator | Name | Equivalent | Example | Result |
|---|---|---|---|---|
| = | Simple Assignment | N/A | x = 5 |
x is 5 |
| += | Addition Assignment | x = x + y |
x += 3 |
Adds and assigns |
| -= | Subtraction Assignment | x = x - y |
x -= 3 |
Subtracts and assigns |
| *= | Multiplication Assignment | x = x * y |
x *= 3 |
Multiplies and assigns |
| /= | Division Assignment | x = x / y |
x /= 3 |
Divides and assigns |
| %= | Remainder Assignment | x = x % y |
x %= 3 |
Remainder and assigns |
| **= | Exponentiation Assignment | x = x ** y |
x **= 3 |
Exponentiates and assigns |
| <<= | Left Shift Assignment | x = x << y |
x <<= 2 |
Left shift and assigns |
| >>= | Right Shift Assignment | x = x >> y |
x >>= 2 |
Right shift and assigns |
| >>>= | Unsigned Right Shift Assignment | x = x >>> y |
x >>>= 2 |
Unsigned shift and assigns |
| &= | Bitwise AND Assignment | x = x & y |
x &= 3 |
AND and assigns |
| |= | Bitwise OR Assignment | x = x | y |
x |= 3 |
OR and assigns |
| ^= | Bitwise XOR Assignment | x = x ^ y |
x |= 3 |
XOR and assigns |
| Feature | Description | Example |
|---|---|---|
| Destructuring Assignment | Unpack values from arrays/objects | [a, b] = [1, 2], {x, y} = obj |
| Multiple Assignment | Chain assignments (right-to-left) | a = b = c = 5 (all get 5) |
| Return Value | Assignment returns assigned value | y = (x = 5) (both are 5) |
| Compound Benefits | Shorter, clearer, evaluates left side once | arr[i++] += 5 increments i once |
Example: Assignment operators
// Simple assignment
let x = 10;
// Compound assignments
x += 5; // x = x + 5; x is now 15
x -= 3; // x = x - 3; x is now 12
x *= 2; // x = x * 2; x is now 24
x /= 4; // x = x / 4; x is now 6
x %= 5; // x = x % 5; x is now 1
x **= 3; // x = x ** 3; x is now 1
// String concatenation with +=
let str = "Hello";
str += " World"; // str is "Hello World"
// Multiple assignment (right-to-left)
let a, b, c;
a = b = c = 5; // All are 5
console.log(a, b, c); // 5 5 5
// Assignment returns value
let y = (x = 10); // x is 10, y is 10
console.log(x, y); // 10 10
// Destructuring assignment
let [p, q] = [1, 2];
console.log(p, q); // 1 2
let {name, age} = {name: "John", age: 30};
console.log(name, age); // "John" 30
// Compound assignment with side effects
let arr = [10, 20, 30];
let i = 0;
arr[i++] += 5; // arr[0] becomes 15, then i increments
console.log(arr[0], i); // 15 1
// Swapping with destructuring
let m = 1, n = 2;
[m, n] = [n, m]; // Swap
console.log(m, n); // 2 1
3. Comparison Operators (==, ===, !=, !==) and Equality Rules
| Operator | Name | Description | Type Coercion | Example |
|---|---|---|---|---|
| == | Equality (loose) | Checks value equality with type coercion | ✓ Yes | 5 == "5" → true |
| === | Strict Equality | Checks value and type equality | ✗ No | 5 === "5" → false |
| != | Inequality (loose) | Checks value inequality with coercion | ✓ Yes | 5 != "6" → true |
| !== | Strict Inequality | Checks value or type inequality | ✗ No | 5 !== "5" → true |
| < | Less Than | Checks if left < right | ✓ Yes | 5 < 10 → true |
| > | Greater Than | Checks if left > right | ✓ Yes | 10 > 5 → true |
| <= | Less Than or Equal | Checks if left ≤ right | ✓ Yes | 5 <= 5 → true |
| >= | Greater Than or Equal | Checks if left ≥ right | ✓ Yes | 10 >= 10 → true |
| Comparison | == (loose) | === (strict) | Reason |
|---|---|---|---|
5 vs "5" |
true |
false |
String coerced to number |
0 vs false |
true |
false |
Boolean coerced to number |
null vs undefined |
true |
false |
Special case: only equal to each other |
"" vs 0 |
true |
false |
Empty string coerced to 0 |
NaN vs NaN |
false |
false |
NaN not equal to itself (use isNaN()) |
[] vs [] |
false |
false |
Different object references |
0 vs -0 |
true |
true |
Considered equal (use Object.is() to distinguish) |
Example: Equality and comparison operators
// Strict equality (===) - recommended
console.log(5 === 5); // true
console.log(5 === "5"); // false (different types)
console.log(null === null); // true
console.log(undefined === undefined); // true
// Loose equality (==) - with type coercion
console.log(5 == "5"); // true (string coerced to number)
console.log(0 == false); // true (boolean to number)
console.log("" == 0); // true (string to number)
console.log(null == undefined); // true (special case)
// Inequality
console.log(5 !== "5"); // true (strict)
console.log(5 != "6"); // true (loose)
// Relational operators
console.log(5 < 10); // true
console.log(10 > 5); // true
console.log(5 <= 5); // true
console.log(10 >= 10); // true
// String comparison (lexicographic)
console.log("apple" < "banana"); // true (a < b)
console.log("10" < "9"); // true (string comparison: "1" < "9")
console.log(10 < "9"); // false (number comparison: 10 > 9)
// Special cases
console.log(NaN === NaN); // false (use Number.isNaN())
console.log(NaN == NaN); // false
console.log(Number.isNaN(NaN)); // true (correct way)
console.log(0 === -0); // true
console.log(Object.is(0, -0)); // false (distinguishes +0 and -0)
// Object comparison (reference)
const obj1 = {a: 1};
const obj2 = {a: 1};
const obj3 = obj1;
console.log(obj1 === obj2); // false (different references)
console.log(obj1 === obj3); // true (same reference)
// Array comparison
console.log([1,2] == [1,2]); // false (different references)
console.log([1,2] === [1,2]); // false
// Tricky comparisons to avoid
console.log([] == ![]); // true (![] is false, [] coerced to "")
console.log([] == false); // true
console.log("" == 0); // true
console.log(" " == 0); // true
console.log("0" == 0); // true
console.log("0" == false); // true
Warning: Always use
=== and !== (strict equality) to avoid unexpected
type coercion bugs. Use == only when you specifically need type coercion.
4. Logical Operators (&&, ||, !) and Short-circuit Evaluation
| Operator | Name | Returns | Short-circuits | Use Case |
|---|---|---|---|---|
| && | Logical AND | First falsy value or last value | ✓ If left is falsy | Both conditions must be true |
| || | Logical OR | First truthy value or last value | ✓ If left is truthy | At least one condition must be true |
| ! | Logical NOT | Boolean (inverted) | ✗ No | Inverts truthiness |
| !! | Double NOT | Boolean conversion | ✗ No | Convert to boolean explicitly |
| Expression | Result | Explanation |
|---|---|---|
| true && true | true |
Both truthy → returns last value |
| true && false | false |
Right is falsy → returns false |
| false && true | false |
Short-circuits at false (left) |
| 5 && "hello" | "hello" |
Both truthy → returns last |
| 0 && "hello" | 0 |
First falsy → returns 0 |
| true || false | true |
Short-circuits at first truthy |
| false || true | true |
Returns first truthy value |
| false || false | false |
Both falsy → returns last |
| 5 || "hello" | 5 |
First truthy → short-circuits |
| 0 || "hello" | "hello" |
First falsy, returns second |
| !true | false |
Inverts boolean |
| !0 | true |
0 is falsy, inverted to true |
| !!"hello" | true |
Convert to boolean: truthy |
Example: Logical operators and short-circuit evaluation
// Logical AND (&&) - returns first falsy or last value
console.log(true && true); // true
console.log(true && false); // false
console.log(5 && 10); // 10 (both truthy, returns last)
console.log(0 && 10); // 0 (first falsy)
console.log(null && "hello"); // null (first falsy)
// Logical OR (||) - returns first truthy or last value
console.log(false || true); // true
console.log(false || false); // false (last value)
console.log(5 || 10); // 5 (first truthy)
console.log(0 || 10); // 10 (first falsy, returns second)
console.log(null || "default"); // "default" (first falsy)
// Logical NOT (!)
console.log(!true); // false
console.log(!false); // true
console.log(!0); // true (0 is falsy)
console.log(!""); // true ("" is falsy)
console.log(!"hello"); // false ("hello" is truthy)
// Double NOT (!!) - convert to boolean
console.log(!!"hello"); // true
console.log(!!0); // false
console.log(!!""); // false
console.log(!!{}); // true (objects are truthy)
// Short-circuit evaluation - practical uses
// 1. Default values (before ?? was added)
let username = input || "Guest"; // If input is falsy, use "Guest"
// 2. Conditional execution
isLoggedIn && showDashboard(); // Execute if truthy
hasError || showSuccessMessage(); // Execute if falsy
// 3. Guard clauses
function processUser(user) {
user && user.profile && console.log(user.profile.name);
// Only accesses nested property if all are truthy
}
// 4. Avoiding function calls
let result = expensiveCheck() || cachedValue;
// If expensiveCheck() is truthy, cachedValue never evaluated
// Short-circuit prevents errors
let obj = null;
let value = obj && obj.property; // undefined (doesn't throw)
// Without short-circuit: obj.property would throw error
// Combining operators
let access = isAdmin || (isPremium && hasPermission);
console.log(true || (false && error())); // true, error() not called
// Falsy values: false, 0, "", null, undefined, NaN
console.log(false || 0 || "" || null || undefined || NaN || "found");
// "found" (first truthy)
Note: Logical operators return the actual value (not just boolean). Short-circuit evaluation
stops at first definitive result, useful for default values and conditional execution.
5. Bitwise Operators (&, |, ^, ~, <<, >>) and Binary Operations
| Operator | Name | Description | Example | Binary | Result |
|---|---|---|---|---|---|
| & | AND | 1 if both bits are 1 | 5 & 3 |
0101 & 0011 |
1 (0001) |
| | | OR | 1 if at least one bit is 1 | 5 | 3 |
0101 | 0011 |
7 (0111) |
| ^ | XOR | 1 if bits are different | 5 ^ 3 |
0101 ^ 0011 |
6 (0110) |
| ~ | NOT | Inverts all bits | ~5 |
~0101 |
-6 (two's complement) |
| << | Left Shift | Shifts bits left, fills 0s | 5 << 1 |
0101 → 1010 |
10 |
| >> | Sign-propagating Right Shift | Shifts bits right, preserves sign | 5 >> 1 |
0101 → 0010 |
2 |
| >>> | Zero-fill Right Shift | Shifts bits right, fills 0s | -5 >>> 1 |
Fills with 0s | Large positive number |
| Use Case | Operation | Example | Purpose |
|---|---|---|---|
| Check if even | n & 1 |
5 & 1 = 1 (odd), 4 & 1 = 0 (even) |
Fast even/odd check |
| Multiply by 2n | n << x |
5 << 2 = 20 (5 × 4) |
Fast multiplication |
| Divide by 2n | n >> x |
20 >> 2 = 5 (20 ÷ 4) |
Fast division |
| Toggle bit | n ^ (1 << i) |
Toggles i-th bit | Flags/permissions |
| Set bit | n | (1 << i) |
Sets i-th bit to 1 | Enable flag |
| Clear bit | n & ~(1 << i) |
Sets i-th bit to 0 | Disable flag |
| Check bit | (n >> i) & 1 |
Gets value of i-th bit | Test flag |
| Swap variables | a ^= b; b ^= a; a ^= b; |
Swap without temp variable | XOR swap trick |
Example: Bitwise operations
// Basic bitwise operations
console.log(5 & 3); // 1 (0101 & 0011 = 0001)
console.log(5 | 3); // 7 (0101 | 0011 = 0111)
console.log(5 ^ 3); // 6 (0101 ^ 0011 = 0110)
console.log(~5); // -6 (inverts bits, two's complement)
// Bit shifts
console.log(5 << 1); // 10 (0101 → 1010) multiply by 2
console.log(5 << 2); // 20 (0101 → 10100) multiply by 4
console.log(10 >> 1); // 5 (1010 → 0101) divide by 2
console.log(20 >> 2); // 5 (10100 → 0101) divide by 4
// Practical use cases
// 1. Check if number is even or odd
function isEven(n) {
return (n & 1) === 0;
}
console.log(isEven(4)); // true
console.log(isEven(5)); // false
// 2. Flags and permissions
const READ = 1; // 001
const WRITE = 2; // 010
const EXECUTE = 4; // 100
let permissions = 0;
permissions |= READ; // Add READ permission
permissions |= WRITE; // Add WRITE permission
console.log(permissions & READ); // Check if READ (non-zero = true)
console.log(permissions & EXECUTE); // Check if EXECUTE (0 = false)
permissions &= ~WRITE; // Remove WRITE permission
console.log(permissions); // 1 (only READ)
// 3. Fast integer conversion
console.log(~~3.14); // 3 (double NOT truncates decimals)
console.log(5.7 | 0); // 5 (OR with 0 truncates)
console.log(5.7 >> 0); // 5 (right shift by 0 truncates)
// 4. XOR swap (without temp variable)
let a = 5, b = 10;
a ^= b; // a = 5 ^ 10
b ^= a; // b = 10 ^ (5 ^ 10) = 5
a ^= b; // a = (5 ^ 10) ^ 5 = 10
console.log(a, b); // 10 5
// 5. Toggle boolean
let flag = true;
flag ^= 1; // false (1 ^ 1 = 0)
flag ^= 1; // true (0 ^ 1 = 1)
// 6. Set specific bit
let n = 0b0000; // Binary literal
n |= (1 << 2); // Set bit 2: 0b0100
console.log(n.toString(2)); // "100"
// 7. Clear specific bit
n &= ~(1 << 2); // Clear bit 2: 0b0000
console.log(n.toString(2)); // "0"
Note: Bitwise operators work on 32-bit integers. Common uses include flags, permissions, fast
math operations, and low-level data manipulation.
6. Optional Chaining (?.) and Nullish Coalescing (??) Operators
| Operator | Syntax | Returns | Use Case |
|---|---|---|---|
| ?. ES2020 | obj?.prop |
undefined if obj is null/undefined, else obj.prop |
Safe property access |
| ?.[] | obj?.[expr] |
undefined if obj is null/undefined |
Safe dynamic property access |
| ?.() | func?.(args) |
undefined if func is null/undefined |
Safe function call |
| ?? ES2020 | a ?? b |
a if not null/undefined, else b |
Default values for null/undefined only |
| Feature | || (OR) | ?? (Nullish Coalescing) |
|---|---|---|
| Falsy values treated as missing | Yes (false, 0, "", null, undefined, NaN) | No (only null/undefined) |
0 || 100 |
100 |
0 |
"" || "default" |
"default" |
"" |
false || true |
true |
false |
null ?? 100 |
100 |
100 |
undefined ?? 100 |
100 |
100 |
| Use case | When any falsy should use default | When only null/undefined should use default |
Example: Optional chaining and nullish coalescing
// Optional Chaining (?.)
// Without optional chaining (verbose and error-prone)
let user = null;
// let name = user.profile.name; // TypeError: Cannot read property 'profile' of null
let name = user && user.profile && user.profile.name; // undefined
// With optional chaining (concise and safe)
name = user?.profile?.name; // undefined (no error)
const user2 = {
profile: {
name: "Alice",
address: {
city: "NYC"
}
}
};
console.log(user2?.profile?.name); // "Alice"
console.log(user2?.profile?.address?.city); // "NYC"
console.log(user2?.profile?.phone); // undefined (no error)
console.log(user2?.account?.balance); // undefined
// Optional chaining with arrays
const arr = null;
console.log(arr?.[0]); // undefined (safe)
console.log(user2?.tags?.[0]); // undefined
// Optional chaining with function calls
const obj = {
method: function() { return "called"; }
};
console.log(obj.method?.()); // "called"
console.log(obj.nonExistent?.()); // undefined (no error)
console.log(user?.getProfile?.()); // undefined
// Nullish Coalescing (??)
// Problem with || operator
let count = 0;
let value1 = count || 10; // 10 (0 is falsy, uses default)
let value2 = count ?? 10; // 0 (0 is not null/undefined)
let text = "";
let msg1 = text || "default"; // "default" ("" is falsy)
let msg2 = text ?? "default"; // "" (empty string is valid)
// Use ?? for actual null/undefined checks
let config = {
timeout: 0, // 0 is valid
enabled: false, // false is valid
name: "" // empty string is valid
};
let timeout = config.timeout ?? 3000; // 0 (not null/undefined)
let enabled = config.enabled ?? true; // false (not null/undefined)
let name = config.name ?? "Unnamed"; // "" (not null/undefined)
let missing = config.missing ?? "N/A"; // "N/A" (undefined)
// Combining ?. and ??
const data = {
user: null,
settings: { theme: "dark" }
};
let theme = data?.settings?.theme ?? "light"; // "dark"
let userName = data?.user?.name ?? "Guest"; // "Guest"
// Real-world example: API response
function getUserCity(response) {
return response?.data?.user?.address?.city ?? "Unknown";
}
console.log(getUserCity(null)); // "Unknown"
console.log(getUserCity({ data: null })); // "Unknown"
console.log(getUserCity({
data: { user: { address: { city: "NYC" } } }
})); // "NYC"
// Short-circuit evaluation
let expensiveFn = () => { console.log("Called"); return 42; };
let result = null?.property?.method?.(); // undefined (expensiveFn not called)
console.log(result); // undefined
Warning: Don't overuse
?. as it can hide bugs. Use it for genuinely optional
properties, not to mask errors in required data.
7. Logical Assignment Operators (||=, &&=, ??=) - ES2021
| Operator | Name | Equivalent | Assigns When |
|---|---|---|---|
| ||= ES2021 | Logical OR Assignment | x || (x = y) |
x is falsy |
| &&= ES2021 | Logical AND Assignment | x && (x = y) |
x is truthy |
| ??= ES2021 | Nullish Assignment | x ?? (x = y) |
x is null or undefined |
| Operator | Behavior | Use Case | Example |
|---|---|---|---|
| ||= | Assigns if current value is falsy | Set default for any falsy value | x ||= 10 (assigns if x is falsy) |
| &&= | Assigns if current value is truthy | Update existing truthy value | x &&= 10 (assigns if x is truthy) |
| ??= | Assigns only if null/undefined | Set default only for null/undefined | x ??= 10 (assigns if x is null/undefined) |
Example: Logical assignment operators
// ||= Logical OR Assignment
// Assigns if left side is falsy
let a = 0;
a ||= 10; // a is 10 (0 is falsy)
let b = 5;
b ||= 10; // b is still 5 (5 is truthy)
let c = "";
c ||= "default"; // c is "default" ("" is falsy)
// &&= Logical AND Assignment
// Assigns if left side is truthy
let x = 5;
x &&= 10; // x is 10 (5 is truthy)
let y = 0;
y &&= 10; // y is still 0 (0 is falsy, no assignment)
let z = null;
z &&= 10; // z is still null (null is falsy)
// ??= Nullish Assignment
// Assigns only if null or undefined
let p = 0;
p ??= 10; // p is still 0 (0 is not null/undefined)
let q = null;
q ??= 10; // q is 10 (null)
let r = undefined;
r ??= 10; // r is 10 (undefined)
let s = false;
s ??= true; // s is still false (false is not null/undefined)
// Practical use cases
// 1. Object property defaults with ??=
const config = {
timeout: 0, // 0 is valid
retry: null // null should be replaced
};
config.timeout ??= 3000; // Still 0 (not null/undefined)
config.retry ??= 3; // Now 3 (was null)
config.newProp ??= "default"; // "default" (was undefined)
console.log(config);
// { timeout: 0, retry: 3, newProp: "default" }
// 2. Accumulation with &&=
let total = 100;
let applyDiscount = true;
total &&= total * 0.9; // Apply 10% discount if total exists
console.log(total); // 90
let nullTotal = null;
nullTotal &&= nullTotal * 0.9; // No operation (nullTotal is falsy)
console.log(nullTotal); // null (unchanged)
// 3. Cache pattern with ||=
let cache = {};
function getData(key) {
// Set cache if not already set
cache[key] ||= expensiveOperation(key);
return cache[key];
}
function expensiveOperation(key) {
console.log("Computing...");
return key * 2;
}
console.log(getData(5)); // "Computing..." then 10
console.log(getData(5)); // 10 (from cache, no computing)
// 4. Updating objects conditionally
const user = {
name: "Alice",
age: null,
status: undefined
};
user.name ||= "Anonymous"; // Still "Alice" (truthy)
user.age ??= 18; // Now 18 (was null)
user.status ??= "active"; // Now "active" (was undefined)
// 5. Short-circuit: right side not evaluated if condition not met
let count = 0;
let getValue = () => { count++; return 100; };
let x1 = 5;
x1 ||= getValue(); // getValue() NOT called (x1 is truthy)
console.log(count); // 0
let x2 = 0;
x2 ||= getValue(); // getValue() IS called (x2 is falsy)
console.log(count); // 1
Note: Logical assignment operators combine logical operations with assignment, providing
cleaner syntax and short-circuit evaluation (right side only evaluated when assignment occurs).
Section 4 Summary
- Arithmetic operators include +, -, *, /, %, ** (exponentiation);
+concatenates strings if either operand is string - Assignment operators support compound forms (+=, -=, *=, etc.) for concise update operations
- Use strict equality (===, !==) to avoid type coercion bugs; loose equality (==) performs type conversion
- Logical operators (&&, ||, !) short-circuit and return actual values, not just booleans; useful for defaults and guards
- Bitwise operators work on 32-bit integers; useful for flags, permissions, and fast math operations
- Optional chaining (?.) safely accesses nested properties; nullish coalescing (??) provides defaults only for null/undefined
- Logical assignment (||=, &&=, ??=) combines logic with assignment; short-circuits when assignment unnecessary