Identity and shared paths
Two bindings can contain a reference to the same object. Mutating a property through one makes the change visible through the other. Creating { ...original } produces a distinct outer object, but values that are references still point to the original nested objects.
Draw the paths original.policy and copy.policy before predicting a retries mutation. Strict equality of the outer objects may be false while equality of the nested objects is true. Do not treat matching fields as proof of identity, or different outer identities as proof of complete isolation.
Own property and available value
A read such as option.enabled may obtain a value from the prototype. That is useful for method reuse, but does not demonstrate that a field arrived on the record itself. Object.hasOwn(option, "enabled") checks ownership. The in operator also includes the prototype chain.
For a rule requiring an own Boolean option, check ownership and value separately: Object.hasOwn(option, "enabled") && option.enabled === true. Here assume a bounded ordinary object without getters or proxies. Do not call option.hasOwnProperty as though that property were necessarily a reliable function: imported data may replace it, or the object may have a null prototype.
Defaults and optional access
In const { timeout = 30 } = input, the default applies when the extracted value is undefined. Null remains null. Decide whether null means permitted absence, an explicit value or a contract error; syntax does not make that decision for you.
A chain such as input?.meta?.name protects both locations that may be null or undefined. In (input?.meta).name, parentheses end the protected chain: the outer read may attempt to access undefined. Even an exception-free read does not validate the type of name. If the destination requires a string, retain that validation as an explicit step.
What spread actually copies
Spread in an object literal traverses enumerable own properties, reads their values and creates data properties on the target. An eligible getter runs during that read; its accessor descriptor is not transferred to the target. The prototype and methods located on it are not copied either.
Order matters: { ...defaults, ...supplied } allows an own property from supplied to replace the earlier value, including with undefined. When reviewing configuration, separate precedence policy, absence handling and validation. Reversing spread order changes precedence but does not by itself resolve an undefined input contract.
Freeze, classes and receivers
Object.freeze acts on the supplied object, preventing extensions and making its data properties non-writable. It neither creates a copy nor traverses nested objects. Writing to a frozen data property throws TypeError in strict mode; writing to a still-mutable nested object may succeed.
A class instance may combine own properties, prototype methods and private elements. Spreading the instance into an ordinary object does not recreate that structure. A method reading this.#count requires a receiver with the corresponding private element; supplying { count: 4 } through call does not satisfy that requirement. Analyze the actual receiver before attributing failure to public field values.
Example: configuration snapshot
The captureSettings example works with a small, previously validated schema. It copies numeric timeout and retries, creates a new policy object and a new tags array. Because tags elements are strings, the new array retains no mutable elements shared with the source.
After changing current.policy.retries to 4 and appending "urgent", saved retains retries equal to 2 and tags equal to ["daily"]. The function is not a generic cloner: an extra field, an object inside tags or a getter would require revisiting the contract. It does not freeze saved either; independence from the source does not prevent later direct edits to the snapshot.
Review driven by mutations
To accept a copy, enumerate mutations that must remain isolated: replacing an outer property, changing a nested field and appending an array element are different operations. Trace each to the object actually modified. Then write the value the historical consumer should continue to observe.
In the questions, distinguish three diagnoses: accidental reference sharing, an inherited property and a receiver incompatible with a method. A solution that merely avoids an exception may still accept incorrect data. A solution that freezes an outer object may still leave nested references shared. The decision must meet the complete contract stated in the scenario.
function captureSettings(settings) {
return {
timeout: settings.timeout,
policy: { retries: settings.policy.retries },
tags: [...settings.tags]
};
}
const current = { timeout: 5, policy: { retries: 2 }, tags: ["daily"] };
const saved = captureSettings(current);
current.policy.retries = 4;
current.tags.push("urgent");Read the excerpt assuming already validated data: numeric timeout and retries, tags as a dense string array, and no other fields. Predict saved.policy.retries and saved.tags after both mutations; explain which references were recreated.
Common pitfalls
Confusing const with immutability; assuming spread clones nested objects; accepting inherited properties as own; expecting freeze to be recursive.
Related topics: Bindings, functions and closures · Arrays, collections and iteration · Errors, validation and failure contracts
Design the ownership and copying contract. A new outer reference does not prove that nested data became independent.
Reference: CopyDataProperties · ECMAScript 2026 (17th edition); browser APIs identified separately