skip to content

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?

level: seniorimportance: must knowfreq 65%

answer

  1. the array is new, the contents are not
  2. copies references, not the things referenced
  3. check identity one level down
  4. one level copied, the rest shared
  5. structuredClone drops functions and prototypes

basics

~20 s

Array 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 lines
javascript
const 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]);    // false

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context