Operators, Destructuring & Equality
Modern JavaScript added operators that remove whole lines of defensive code (?., ??, ??=) and syntax that unpacks data in one step (destructuring and spread). This chapter also settles the == vs === question for good.
You will learn
- Arithmetic quirks:
**,%with negatives,+with strings. &&and||return operands; optional chaining?.and nullish coalescing??ES2020.- Logical assignment
??=,||=,&&=ES2021. - Array and object destructuring, and spread ES2015 ES2018.
==,===andObject.is: three kinds of sameness.
Arithmetic and string operators
**ES2016 is right-associative:2 ** 3 ** 2is2 ** 9.%is a remainder: its sign follows the left operand (like Java, unlike Python's modulo).+concatenates as soon as either side is a string; the other arithmetic operators convert to numbers.
β οΈ Pitfall: -2 ** 2 is a SyntaxError. JavaScript refuses to guess whether you mean (-2) ** 2 or -(2 ** 2), so add parentheses.
Logical operators return values
a || b returns a if it's truthy, otherwise b. a && b returns a if it's falsy, otherwise b. Both short-circuit: the right side isn't evaluated when it isn't needed.
Nullish coalescing ?? ES2020
a ?? b returns b only when a is null or undefined. Unlike ||, it keeps 0, "" and false.
β || treats 0 as missing
β ?? only replaces null/undefined
You can't mix ?? with || or && without parentheses: a || b ?? c is a SyntaxError. Write (a || b) ?? c.
Optional chaining ?. ES2020
obj?.prop returns undefined instead of throwing when obj is null or undefined. It also works for computed access (obj?.[key]) and calls (fn?.()). Once it short-circuits, the rest of the chain is skipped.
π‘ Don't overuse ?.. Use it where a value may legitimately be missing. If something should always exist, plain . is better, because a TypeError shows the bug early instead of hiding it as undefined.
Logical assignment ES2021
| Operator | Means | Assigns when a is⦠|
|---|---|---|
a ??= b | a ?? (a = b) | null or undefined |
a ||= b | a || (a = b) | falsy |
a &&= b | a && (a = b) | truthy |
Other operators worth knowing
| Operator | Example | Notes |
|---|---|---|
typeof | typeof x === "string" | safe on undeclared names (outside the TDZ) |
instanceof | e instanceof TypeError | walks the prototype chain (Chapter 11) |
in | "price" in book | own or inherited key; #x in obj for private fields |
delete | delete book.draft | removes a property; doesn't work on variables |
void | void 0 | always undefined (old idiom) |
| comma | (a++, b) | evaluates both, returns the last; rarely good style |
| conditional | qty > 0 ? "in stock" : "sold out" | the only ternary operator |
Destructuring ES2015
Destructuring pulls values out of arrays (by position) or objects (by name) into variables, with optional renaming, defaults and rest. Like default parameters, destructuring defaults apply only for undefined.
Arrays
Objects
In function parameters
Destructuring a parameter object is the idiomatic way to take "named arguments" with defaults. The trailing = {} lets callers omit the argument entirely.
β οΈ Pitfall: destructuring null or undefined throws: const { a } = null gives a TypeError. Defaults in the pattern don't help; give the whole parameter a default (= {}), or use ?? {}.
Spread ES2015 ES2018
... in an array literal, an object literal or a call expands a value. In a destructuring pattern or a parameter list, the same syntax is rest, which collects values.
Equality and sameness
JavaScript has three equality algorithms, plus a fourth used internally by some built-ins:
| Algorithm | Used by | Converts types? | NaN equals NaN? | +0 equals -0? |
|---|---|---|---|---|
| Loose equality | == | yes | no | yes |
| Strict equality | ===, switch, indexOf | no | no | yes |
| SameValueZero | includes, Map/Set keys | no | yes | yes |
| SameValue | Object.is | no | yes | no |
Why == surprises people
== converts its operands before comparing, following a set of rules. null and undefined equal each other and nothing else. Otherwise, objects are converted to primitives and then to numbers, and booleans become numbers first. The results are hard to predict:
π‘ Rule: always use === and !==. The single common exception is x == null, which checks for "null or undefined" in one go.
β Java: === on objects compares references, like Java's == on objects. There's no built-in equals() for value comparison of objects; you compare fields, or serialize the objects.
More examples
1. Safe nested config reading
2. Swapping and rotating with destructuring
3. Removing a key immutably with rest
4. Counting with ??= in a dictionary
5. Returning several values from a function
Recap
**is right-associative and needs parentheses with unary minus.%follows the left operand's sign.+with a string concatenates.||/&&return operands.??replaces onlynull/undefined.?.stops atnull/undefined.??=,||=and&&=assign conditionally.- Destructuring unpacks by position or by name, with renames, defaults (for
undefinedonly) and rest. Spread copies shallowly, and later keys win. - Use
===(andx == null).Object.isdistinguishesNaNand-0;includesandMapuse SameValueZero.
Exercises
Exercise 1 π’ easy predict the output
Evaluate: 0 || "x", 0 ?? "x", "" && "y", null ?? undefined ?? 5
Show solution
"x", 0, "", 5. ?? skips only null and undefined, and can be chained.
Exercise 2 π’ easy refactor it
Replace this with optional chaining and ??: const city = user && user.address && user.address.city ? user.address.city : "n/a";
Show solution
Note the change in behaviour: the original turned an empty city into "n/a". Decide which you want; || would keep the old behaviour.
Exercise 3 π’ easy build it
Using destructuring, write fullName({ first, last = "" }), which returns "Ana Lima" or just "Ana" (no trailing space).
Show solution
The default fills in a missing last; trim() removes the leftover space.
Exercise 4 π’ easy predict the output
True or false? "5" == 5, "5" === 5, null == 0, undefined == null, NaN == NaN
Show solution
true, false, false, true, false. null loosely equals only undefined (and itself); NaN equals nothing, not even itself.
Exercise 5 π‘ medium predict the output
What does this print?
Show solution
null 2 3 then 0 y. Defaults apply only for undefined (or a missing position): null and 0 are real values.
Exercise 6 π‘ medium fix the bug
Calling connect() with no argument throws TypeError: Cannot destructure property 'host' of 'undefined'. Fix it.
Show solution
Defaults inside the pattern apply to the properties. The = {} default for the whole parameter covers a missing argument.
Exercise 7 π‘ medium build it Β· playground
Create playground/settings.js exporting resolveSettings(user), which merges DEFAULTS ({ currency: "EUR", pageSize: 20, theme: "auto" }) with user settings, without letting null or undefined user values override a default, and keeping 0. Don't mutate either input.
Show solution
A plain { ...DEFAULTS, ...user } would let theme: null override "auto". The per-key ?? handles that, and 0 survives.
Exercise 8 π‘ medium explain why
Why is [] == ![] true?
Show solution
![] is false, because [] is truthy. Then [] == false: the boolean becomes 0, and the array is converted to a primitive, "", then to a number, 0. So 0 == 0 is true. That's exactly why === is the default.
Exercise 9 π΄ hard predict the output
What does this print?
Show solution
Emma { user: 'Ana' } true. The spread copied cart by reference, so pushing through next also changed state's items. The nested destructuring skips the first item and takes the second. others holds everything except cart.
Exercise 10 π΄ hard mini-project Β· playground
Create playground/update.js exporting setIn(obj, path, value), which returns a new object with the value at the dotted path (like "customer.address.city") replaced. It copies only the objects along the path, and creates missing ones. Show that the original is untouched and that unrelated branches are shared.
Show solution
It combines array destructuring with rest, computed keys, object spread for copy-on-write, ?. plus ?? for missing branches, and recursion. This is the same pattern state libraries use for immutable updates.