Chapter 5 Β· Expressions & Built-in Data

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.
  • ==, === and Object.is: three kinds of sameness.

Arithmetic and string operators

⚠️ 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

OperatorMeansAssigns when a is…
a ??= ba ?? (a = b)null or undefined
a ||= ba || (a = b)falsy
a &&= ba && (a = b)truthy

Other operators worth knowing

OperatorExampleNotes
typeoftypeof x === "string"safe on undeclared names (outside the TDZ)
instanceofe instanceof TypeErrorwalks the prototype chain (Chapter 11)
in"price" in bookown or inherited key; #x in obj for private fields
deletedelete book.draftremoves a property; doesn't work on variables
voidvoid 0always undefined (old idiom)
comma(a++, b)evaluates both, returns the last; rarely good style
conditionalqty > 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:

AlgorithmUsed byConverts types?NaN equals NaN?+0 equals -0?
Loose equality==yesnoyes
Strict equality===, switch, indexOfnonoyes
SameValueZeroincludes, Map/Set keysnoyesyes
SameValueObject.isnoyesno

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 only null/undefined. ?. stops at null/undefined.
  • ??=, ||= and &&= assign conditionally.
  • Destructuring unpacks by position or by name, with renames, defaults (for undefined only) and rest. Spread copies shallowly, and later keys win.
  • Use === (and x == null). Object.is distinguishes NaN and -0; includes and Map use 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.

Official docs