Defaults, rest, and copying without surprises
When a destructuring default applies, how spread order decides who wins, and what freeze, JSON and structuredClone really copy.
After this lesson you can
- Say exactly when a default value applies, and why null skips it
- Remove keys with object rest and merge objects with spread in the right order
- Choose between a spread copy, structuredClone and a JSON round trip
- Explain why Object.freeze is shallow, and freeze every level
Lesson 3 introduced destructuring and showed that spread copies one level deep. This lesson covers the rules that decide what you actually get.
Defaults apply only to undefined
A default in a destructuring pattern or a parameter list is used when
the value is undefined — missing, or explicitly undefined. null is
a value, so it skips the default and comes through as null.
function connect({ host = "localhost", port = 5432 } = {}) {
return `${host}:${port}`;
}
connect(); // "localhost:5432" — the = {} covers no argument
connect({ port: null }); // "localhost:null"
The same split shows up with operators: ?? falls back for null and
undefined, || for every falsy value, including 0 and "".
Rest and spread
Rest collects "everything else". In an object pattern it is the easy way to drop keys without mutating anything:
const { password, ...publicUser } = user; // publicUser has no password
Spread copies own enumerable properties into a new object, in order.
When two sources share a key, the later one wins, so defaults go first
and overrides last: { ...defaults, ...options }. A key present in
options with the value undefined still overwrites the default.
function connect({ host = "localhost", port = 5432 } = {}) { return `${host}:${port}`;} export function run() { const defaults = { retries: 3, verbose: false }; const user = { id: 1, name: "Ana", password: "hunter2" }; const { password, ...publicUser } = user; const withUndefined = { ...defaults, ...{ retries: undefined } }; return { noArgument: connect(), undefinedPort: connect({ port: undefined }), nullPort: connect({ port: null }), laterWins: { ...defaults, ...{ verbose: true } }, earlierLoses: { ...{ verbose: true }, ...defaults }, undefinedStillOverwrites: String(withUndefined.retries), publicUser, zeroOr: 0 || 10, zeroNullish: 0 ?? 10, };}Deep copies
Spread is shallow: nested objects are shared (lesson 3). For a real deep copy there are two common tools, and they are not equivalent.
JSON.parse(JSON.stringify(x))only keeps what JSON can express. ADatebecomes a string, aMaporSetbecomes{}, keys whose value isundefinedor a function disappear, and a cycle throws.structuredClone(x)copiesDate,Map,Set, typed arrays and cycles correctly. It throws on functions and does not keep a class instance's prototype — you get a plain object back.
freeze is shallow too
Object.freeze(obj) stops properties of obj itself from being added,
removed or changed. In strict code a write throws a TypeError; in
sloppy code it is silently ignored. Nested objects are not frozen;
freezing every level takes a recursive walk, which is the exercise.
"use strict"; export function run() { const original = { when: new Date(0), tags: new Set(["a", "b"]), nested: { n: 1 }, note: undefined, }; const viaJson = JSON.parse(JSON.stringify(original)); const viaClone = structuredClone(original); original.nested.n = 2; let cloneFunction; try { structuredClone({ greet() {} }); } catch (e) { cloneFunction = e.name; } const frozen = Object.freeze({ level: 1, inner: { x: 1 } }); let writeFrozen; try { frozen.level = 2; } catch (e) { writeFrozen = e.name; } frozen.inner.x = 2; return { jsonWhen: typeof viaJson.when, jsonTags: viaJson.tags, jsonKeepsNote: "note" in viaJson, cloneWhenIsDate: viaClone.when instanceof Date, cloneTagsIsSet: viaClone.tags instanceof Set, cloneNested: viaClone.nested.n, cloneKeepsNote: "note" in viaClone, cloneFunction, writeFrozen, frozenInnerX: frozen.inner.x, };}Common mistakes
- Expecting a default to replace
null. An API that sends"port": nullgetsnull, not the default; use??ifnullshould fall back too. { ...options, ...defaults }. The order is reversed: the defaults overwrite everything the caller passed.- JSON round trip as a general deep clone. Dates come back as strings and Maps as empty objects — the second example shows both.
Object.freezeon config with nested sections. The nested sections are still writable.
What interviewers ask
- What is the difference between
??and||? —??falls back only onnull/undefined;||on any falsy value, so0 || 10is10but0 ?? 10is0.
Exercise · Pro
Freeze all the way down
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade