You copy an array of objects with slice(), then edit a property on one element of the copy — and the original array shows the change too. Why, and how do you get a copy that does not share nested data?
answer
- the array is new, the contents are not
- copies references, not the things referenced
- check identity one level down
- one level copied, the rest shared
- structuredClone drops functions and prototypes
basics
~20 sArray copy methods such as slice, concat and map are shallow: they copy element references, so nested objects remain shared between the two arrays and editing one shows through both. A deep copy needs structuredClone or explicit per-level copying.
solid answer
~50 s`slice()` produces a new outer array, but the elements it stores are the same *references* the original held. Object elements are therefore shared: `copy[0]` and `original[0]` are literally the same object, so `copy[0].done = true` is visible from both arrays. The array identity test misleads people here — `copy !== original` is true, yet nothing below the top level was duplicated. The same is true of `concat`, `map` when the callback returns the element, and the ES2023 copying methods. To break the sharing you either copy each level you intend to edit — `arr.map(item => ({ ...item }))` for one nested level — or take a real deep copy with `structuredClone(arr)`, which handles nesting and cycles and clones `Date`, `Map`, `Set` and typed arrays, but throws a `DataCloneError` on functions and returns plain objects rather than class instances. Choose the shallowest copy that covers what you actually mutate; deep-cloning everything is wasteful on hot paths.
code
javascript · 15 linesconst original = [{ id: 1, done: false }];
const shallow = original.slice();
console.log(shallow === original); // false
console.log(shallow[0] === original[0]); // true — shared
shallow[0].done = true;
console.log(original[0].done); // true — leaked
original[0].done = false;
const oneLevel = original.map(item => ({ ...item }));
oneLevel[0].done = true;
console.log(original[0].done); // false — isolated
const deep = structuredClone(original);
console.log(deep[0] === original[0]); // falsego deeper
Know that copying an array copies references to its objects, so changing a property on an element of the copy is visible in the original.
Explain why the outer array is independent while the elements are shared, and show the one-level fix with map plus object spread for arrays of flat objects.
Show diagnosis and judgment: confirm the sharing with an identity check, pick the shallowest copy that covers the mutation, and name structuredClone's cost and its DataCloneError and prototype-loss limits.
Own the data-ownership policy — where copies happen in the system, whether deep cloning is affordable at your data volumes, and how freezing or immutable structures keep the invariant enforceable rather than remembered.
## What "shallow" actually means JavaScript array elements are *values*. For primitives that value is the number or string itself; for objects the value is a **reference** to an object living elsewhere in memory. A copying array method duplicates the container and its slots — not whatever the slots point at. ```js const original = [{ id: 1, done: false }, { id: 2, done: false }]; const copy = original.slice(); console.log(copy === original); // false — different arrays console.log(copy[0] === original[0]); // true — same object copy[0].done = true; console.log(original[0].done); // true — shows through ``` So two things are true at once: you made a real copy, and you did not isolate the data. That is the whole confusion. ## Which operations are shallow Essentially all of them: `slice()`, `concat()`, `Array.from(arr)`, `map(x => x)`, `filter(...)`, `flat()`, and the ES2023 `toSorted`/`toReversed`/`toSpliced`/`with`. None of them recurses into elements. What a shallow copy *does* protect is the array's own structure. After `const copy = original.slice()`, calling `copy.push(x)`, `copy.reverse()` or replacing `copy[0] = newObj` has no effect on `original`, because those operations change slots in the new array. Only reaching *through* a slot into the shared object leaks. ## Copying the levels you edit If you know how deep your edits go, copy exactly that far. For an array of flat objects, one level is enough: ```js const copy = original.map(item => ({ ...item })); copy[0].done = true; console.log(original[0].done); // false — isolated ``` A targeted update is cheaper still: copy the array, and replace only the one element you are changing with a fresh object. ```js function markDone(list, id) { return list.map(item => (item.id === id ? { ...item, done: true } : item)); } ``` Every untouched element stays shared, which is fine because nothing mutates it — this is the standard structural-sharing trade. ## structuredClone for real deep copies When the shape is arbitrary or deeply nested, `structuredClone(value)` performs a genuine deep copy using the structured clone algorithm. ```js const deep = structuredClone(original); deep[0].done = true; console.log(original[0].done); // false ``` Its strengths: it handles cyclic references without infinite recursion, and it clones `Date`, `RegExp`, `Map`, `Set`, `ArrayBuffer` and typed arrays as those types rather than flattening them. Its limits matter as much as its strengths: - Functions, and objects holding them, cause a `DataCloneError`. So do DOM nodes and other non-serialisable host objects. - **Prototypes are not preserved.** Cloning an instance of a class yields a plain object with the same own properties; `instanceof` fails afterwards. - Getters, setters and property descriptors are not carried over — only own enumerable data properties are cloned, as values. - Symbol-keyed properties are dropped, and symbols themselves cannot be cloned. It is a global function in modern browsers and in Node 17 and later. ## Choosing a strategy Rank by cost and by how much you actually mutate: 1. **Shallow copy** when you only reorder, add, remove or *replace whole elements*. Cheapest. 2. **Copy one nested level** (`map` with object spread) when you edit properties on elements. Still cheap and predictable. 3. **structuredClone** when the data is arbitrarily deep or cyclic and you cannot enumerate the levels — and only when the values are plain data. Deep-cloning defensively on every read is a real performance problem on large collections, and it silently destroys class instances, so it is not a free "just be safe" default. ## How to diagnose the bug in the wild The symptom is a value changing in a place that never assigned to it. The fast confirmation is an identity check at the boundary — log `a[0] === b[0]` for the two arrays that should be independent. If that is `true`, you have a shallow copy where you needed depth. Freezing the source with `Object.freeze` during development converts the silent write into a thrown `TypeError` at the exact line, which turns a hunt into a stack trace.
- If the copy is shallow, why is copy.push(x) still safe?Because `push` writes to a slot in the *new* array. Shallow copying does isolate the array's own structure — length, order and which reference sits in each slot. Only dereferencing a shared element, as in `copy[0].done = true`, reaches data that both arrays point at.
- What happens if you structuredClone an array of class instances?It succeeds if the instances hold only cloneable data, but the results are plain objects: the structured clone algorithm copies own enumerable properties and does not preserve prototypes, so methods are gone and `instanceof` returns false. If any property holds a function you get a `DataCloneError` instead.
- How do you make an accidental nested mutation fail loudly instead of silently?Freeze the source during development: `Object.freeze(original)` makes writes to its own properties throw a `TypeError` in strict mode, which module code always is. Freezing is shallow too, so deep protection means freezing recursively — usually worth doing only in development builds because of the cost.
saying these in an interview costs you the question
- Saying slice() gives you an independent copy of everything
- Thinking copy !== original proves the data is isolated
- Reaching for structuredClone on every copy by default
- Believing structuredClone keeps class instances intact
- Claiming spread copies one more level than slice does