A JavaScript object exposes `fullName` through a getter. You copy the object with `{ ...user }` — what does the copy actually contain, and how do you copy it so the accessor survives?
answer
- copying reads, it does not describe
- the getter fires exactly once
- a snapshot, not a live derivation
- non-enumerables and the prototype drop out
- descriptors in, descriptors out
basics
~20 sObject spread reads each own enumerable property, so the getter runs once and its result is stored as a plain data property — the copy is a frozen snapshot. Use Object.getOwnPropertyDescriptors to copy descriptors instead.
solid answer
~40 sBoth `{ ...user }` and `Object.assign({}, user)` copy own **enumerable** properties by *reading* them, so an accessor is invoked once and whatever it returned is written into the copy as an ordinary writable data property. The copy therefore stops tracking its source fields: change `user.first` and the copy's `fullName` is stale, and assigning to it no longer runs any validation the setter performed. Non-enumerable own properties are dropped entirely, and the prototype is not carried over — the result is a plain object. To copy descriptors instead of values, use `Object.create(Object.getPrototypeOf(user), Object.getOwnPropertyDescriptors(user))`, which reproduces getters, setters and the writable/enumerable/configurable flags, and keeps the prototype. For merging into an existing target, `Object.defineProperties(target, Object.getOwnPropertyDescriptors(source))` does the same job. `Object.getOwnPropertyDescriptors` is ES2017 and object spread is ES2018; both are baseline in current browsers and Node.
code
javascript · 20 linesconst user = {
first: 'Ada',
last: 'Lovelace',
get fullName() { return `${this.first} ${this.last}`; },
};
const spread = { ...user };
const clone = Object.create(
Object.getPrototypeOf(user),
Object.getOwnPropertyDescriptors(user),
);
spread.first = 'Augusta';
clone.first = 'Augusta';
console.log(spread.fullName); // 'Ada Lovelace' — frozen snapshot
console.log(clone.fullName); // 'Augusta Lovelace' — accessor preserved
console.log(Object.getOwnPropertyDescriptor(spread, 'fullName').value); // 'Ada Lovelace'
console.log(typeof Object.getOwnPropertyDescriptor(clone, 'fullName').get); // 'function'go deeper
Know that spreading an object copies the values it can read right now, so a computed property becomes a fixed value in the copy rather than staying computed.
Explain the mechanism — copying performs reads, so the getter runs once and the result lands as a data property — and name Object.getOwnPropertyDescriptors as the descriptor-level alternative.
Show that you would recognise this from a bug report: a derived field that is correct on first render and stale afterwards. Also weigh whether generic copying belongs on a type with behaviour at all, versus an explicit clone method.
Own the boundary policy: decide where objects in your system may be copied structurally and where a type must supply its own copy operation, so that invariants enforced by setters cannot be laundered away by a spread at the edge.
## What spread and `Object.assign` actually do Both operations enumerate the source's own enumerable keys — strings and symbols — and for each one perform an ordinary **read**, then store the resulting value in the target. Nothing about the property's descriptor travels; only the value it produced at that instant. ```js const user = { first: 'Ada', last: 'Lovelace', get fullName() { return `${this.first} ${this.last}`; }, }; const copy = { ...user }; Object.getOwnPropertyDescriptor(copy, 'fullName'); // { value: 'Ada Lovelace', writable: true, enumerable: true, configurable: true } user.first = 'Augusta'; console.log(user.fullName); // 'Augusta Lovelace' console.log(copy.fullName); // 'Ada Lovelace' — snapshot, not a live accessor ``` The getter ran exactly once, during the copy. The copy now holds a plain string. Three consequences follow, and each is a real bug people ship: 1. **Staleness.** The derived value no longer derives from anything; it silently drifts out of sync with the fields it was computed from. 2. **Lost validation.** If the source had a setter enforcing an invariant, the copy accepts any assignment without complaint. 3. **Unexpected work, or an unexpected throw.** An expensive getter runs during what looked like a cheap copy, and a getter that throws makes the copy itself throw — often at a call site that has no idea an accessor was involved. ## What else is lost - **Non-enumerable own properties** are skipped, because both operations walk enumerable keys only. Hidden metadata installed with `Object.defineProperty` disappears. - **The prototype is not copied.** `{ ...instance }` produces a plain object whose prototype is `Object.prototype`, so class methods and prototype accessors are gone and `instanceof` no longer holds. That is why spreading a class instance yields a lifeless data bag. - **Descriptor flags are reset.** Even for surviving data properties, the copy's properties are writable, enumerable and configurable regardless of what the source's flags were, because they are created by ordinary definition on a fresh object. Enumerable **symbol-keyed** properties, by contrast, *are* copied by both spread and `Object.assign` — a detail people frequently get backwards. ## The descriptor-preserving copy The fix is to copy the records, not the values: ```js const clone = Object.create( Object.getPrototypeOf(user), Object.getOwnPropertyDescriptors(user), ); Object.getOwnPropertyDescriptor(clone, 'fullName'); // { get: [Function: get fullName], set: undefined, enumerable: true, configurable: true } user.first = 'Augusta'; console.log(clone.first); // 'Ada' — own data copied by value clone.first = 'Augusta'; console.log(clone.fullName); // 'Augusta Lovelace' — accessor still live ``` `Object.getOwnPropertyDescriptors(obj)` (ES2017) returns a map from every own key to its full descriptor, including non-enumerable ones and symbols. `Object.create(proto, descriptorMap)` builds a new object with that prototype and those exact descriptors. The pair is the standard recipe for a faithful shallow clone. For merging into an object that already exists, use the same descriptor map with `Object.defineProperties`: ```js Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); ``` ## `Object.assign` has one extra hazard Spread creates properties on a brand-new object by definition, so the target's own setters can never be involved. `Object.assign` instead performs an ordinary **assignment** on the target, which means an existing setter on the target — or on the target's prototype — runs for each copied key, and a non-writable target property makes the whole call throw a `TypeError` in strict mode partway through, leaving the target half-updated. When the target is a class instance rather than `{}`, that difference is worth knowing before you reach for `Object.assign`. ## Still shallow Neither approach deep-copies. Nested objects are shared by reference in every version above; the descriptor recipe simply guarantees that the *properties* of the top-level object are reproduced faithfully rather than flattened. ## How to answer this in an interview State the mechanism first — spread performs reads, so accessors collapse into values — then name the three losses (accessor-ness, non-enumerable keys, prototype), then give the `getOwnPropertyDescriptors` recipe. Finish with the pragmatic note: if a type has real behaviour, prefer an explicit factory or a `clone()` method over generic copying, because a spread of a rich object quietly produces something that has the same keys and none of the same guarantees.
- Does `Object.assign` behave identically to spread when the target already has properties?No. `Object.assign` performs assignment on the target, so an existing setter on the target or its prototype runs for each key, and a non-writable target property throws a `TypeError` in strict mode partway through, leaving the target half-populated. Object spread defines properties on a brand-new object, so no target setter can ever intercept the copy.
- Besides accessors, what else does object spread quietly leave behind?Non-enumerable own properties, because only enumerable keys are walked, and the prototype — the result is always a plain object, so a spread class instance loses its methods and fails `instanceof`. Descriptor flags are reset to all-true as well. Enumerable symbol-keyed properties, however, are copied, which is the part people usually assume is dropped.
- How would you preserve accessors while merging a source into an object that already exists?Use `Object.defineProperties(target, Object.getOwnPropertyDescriptors(source))`. It applies each descriptor by definition rather than by assignment, so getters and setters arrive intact, non-enumerable keys come along, and the target's own setters are bypassed instead of being triggered mid-merge.
saying these in an interview costs you the question
- Thinks the copy keeps a live link to the getter
- Assumes spread copies the prototype and keeps instanceof working
- Says non-enumerable properties are copied like any other
- Believes spread and Object.assign are always interchangeable
- Calls a descriptor-preserving copy a deep copy