How to copy an array in JavaScript (shallow vs deep)

JavaScript copy array: shallow copies with spread, slice, Array.from, concat, map, and filter; deep copies with structuredClone; JSON limitations; sparse-array caveats. Snippets include short expected-output notes from Node runs.

Published

Updated

Read time 6 min read

Reviewed byDeepak Prasad

How to copy an array in JavaScript (shallow vs deep)

When you need a javascript array copy or js array copy—to sort, dedupe, or mutate without touching the source—most snippets only perform a shallow duplicate. That is fine for primitives (numbers, strings, booleans): the new array holds its own values, so changing the original’s slots does not rewrite the copy. For objects or nested arrays, shallow methods reuse the same inner references, so a javascript clone array plan must add deep copy tooling when you need true isolation (javascript deep copy array vs javascript shallow copy array).

Tested on: Node.js v20.18.2. A short note after each runnable snippet describes what you should see in the console.


Quick reference

New outer array + same inner references = shallow; structuredClone duplicates nested plain data when types are supported.

Approach Typical use
[...arr], arr.slice(), Array.from(arr) Shallow javascript array copy for primitives or when shared objects are intentional
map / filter New array because you are transforming or subsetting, not only duplicating
structuredClone(arr) Deep tree of structured-cloneable data (javascript deep copy array)
JSON.parse(JSON.stringify(...)) JSON-only payloads; know the limitations before you rely on it for javascript clone array trees

1. Spread operator (...)

The spread operator is the most readable copy array javascript pattern for dense arrays: const copy = [...original] creates a new array and copies enumerable elements one level. It is the default people mean by javascript copy array in modern codebases.

javascript
const originalArray = [1, 2, 3];
const copiedArray = [...originalArray];

console.log(copiedArray);

originalArray[0] = 10;
originalArray[1] = 20;
originalArray[2] = 30;

console.log(copiedArray);
console.log(originalArray);
console.log(originalArray === copiedArray);
Output

You should see four lines: two times [ 1, 2, 3 ], then [ 10, 20, 30 ], then false (the copy keeps primitive values; === confirms different array objects).


2. Array.prototype.slice()

Calling slice() with no arguments is the classic array copy javascript idiom: it returns a new array with shallow copies of the elements in the same order. Behavior on sparse arrays differs from spread—see the sparse section later and the FAQ.

javascript
const originalArray = [1, 2, 3];
const copiedArray = originalArray.slice();

console.log(copiedArray);

originalArray[0] = 3;
originalArray[1] = 4;
originalArray[2] = 6;

console.log(copiedArray);
console.log(originalArray);
Output

You should see three lines: [ 1, 2, 3 ] twice, then [ 3, 4, 6 ] for the mutated original while the slice copy stays [ 1, 2, 3 ].


3. Array.from()

Array.from materializes a new array from an iterable or array-like value—another explicit javascript array copy when you want a real Array instance (for example from a NodeList) or a predictable materialization step in copy array js utilities.

javascript
const originalArray = [1, 2, 3];
const copiedArray = Array.from(originalArray);

console.log(copiedArray);
Output

You should see one line: [ 1, 2, 3 ].


4. Array.prototype.concat()

concat with an empty array (or the pattern below) yields a new array with the same shallow references as the source—useful when a pipeline already ends in concat or you prefer a non-spread style for js array copy in older style guides.

javascript
const originalArray = [4, 5, 6];
const copiedArray = [].concat(originalArray);

console.log(copiedArray);

originalArray[0] = 90;
originalArray[1] = 91;
originalArray[2] = 92;

console.log(copiedArray);
Output

You should see two lines: [ 4, 5, 6 ] twice (primitives copied by value into new slots).


5. Array.prototype.map()

map((x) => x) returns a new array and runs your callback per index. For primitives it behaves like other shallow copies; for sparse arrays it is not interchangeable with slice without reading the caveats in the FAQ—prefer spread or slice for a plain javascript shallow copy array unless you are transforming values.

javascript
const originalArray = [1, 2, 3];
const copiedArray = originalArray.map((x) => x);

console.log(copiedArray);

originalArray[0] = 100;

console.log(copiedArray);
Output

You should see two lines, both [ 1, 2, 3 ] (the mapped copy does not pick up the later mutation of index 0 because values were copied at map time).


6. Array.prototype.filter()

filter returns a new array containing only elements that pass your test—here, even numbers. Surviving entries are still shallow copies of references: object elements point at the same instances as in the source, which matters when you chain transforms after copying.

javascript
const originalArray = [1, 2, 3, 4];
const copiedArray = originalArray.filter((x) => x % 2 === 0);

console.log(copiedArray);
Output

You should see one line: [ 2, 4 ].


Shallow copies and nested objects

For objects inside the array, spread, slice, Array.from, map, filter, and concat only copy the outer array container. Inner objects stay shared—both the original and the copy array js view the same mutation unless you deep clone.

javascript
const nested = [{ x: 1 }, { x: 2 }];
const shallowCopy = [...nested];

nested[0].x = 99;

console.log(nested);
console.log(shallowCopy);
Output

You should see two identical lines: both arrays show { x: 99 } in the first slot.


Deep copy with structuredClone

For a true javascript deep copy array of plain nested data, structuredClone duplicates nested objects and arrays without the JSON limitations (MDN). It still cannot clone everything (some platform objects or prototypes); when you hit those edges, use libraries or domain-specific logic. A broader object-oriented discussion lives in the JavaScript deep copy guide.

javascript
const nested = [{ x: 1 }, { x: 2 }];
const deepCopy = structuredClone(nested);

nested[0].x = 99;

console.log(nested);
console.log(deepCopy);
Output

You should see two lines: the first row shows x: 99, the deep copy still shows x: 1 for the first object.


Deep copy with JSON.parse(JSON.stringify(...))

JSON serialization is a common hack for javascript clone array trees of JSON-safe data. It drops undefined, functions, Symbol keys, circular graphs, and weakens Date and Map/Set fidelity—so treat it as a narrow tool, not a general deep cloner for copy an array javascript workflows that include rich types.

javascript
const originalArray = [1, 2, [3, 4], { a: 5, b: 6 }];
const copiedArray = JSON.parse(JSON.stringify(originalArray));

console.log(copiedArray);
Output

You should see one line: [ 1, 2, [ 3, 4 ], { a: 5, b: 6 } ].


Sparse arrays: spread vs slice vs map

Sparse arrays (arrays with “holes”) behave differently depending on which copy API you pick. On Node v20, spread fills a hole with undefined, while slice preserves the hole, and map skips holes but keeps them empty in the result:

javascript
const sparse = [1, , 3];

console.log([...sparse]);
console.log(sparse.slice());
console.log(sparse.map((x) => x));
Output

You should see three lines: spread shows undefined in the hole; slice and map may show <empty item> style holes in Node’s formatter (see console for exact wording).


Summary

Shallow tools give a new array but shared nested objects; structuredClone is the native deep path for structured-cloneable trees; JSON is a narrow hack.

  • Shallow javascript copy array tools ([...arr], slice, from, concat) give a new outer array but share nested object references—fine for primitives, risky for nested state.
  • Use map / filter when the goal is a derived array, not only a duplicate; check sparse-array semantics against slice/spread.
  • Prefer structuredClone for javascript deep copy array graphs of structured-cloneable plain data.
  • Reserve JSON round-trip for JSON-only payloads; it is not a full javascript clone array solution.
  • On sparse arrays, verify array copy javascript behavior (holes vs undefined) before shipping.

References

MDN and on-site guides for spread, slice, structuredClone, and the wider deep-copy topic behind copy array javascript examples.


Frequently Asked Questions

1. What is the best way to copy an array in JavaScript?

For a shallow copy of a normal dense array, spread ([...arr]) or slice() are common and readable. Use structuredClone(arr) when you need a deep copy of nested objects and arrays (where supported). Avoid JSON.parse(JSON.stringify(...)) as a general deep clone—it drops functions, undefined, special numeric values, prototype chains, circular references, and more.

2. Does copying an array copy the objects inside it?

No. Spread, slice, Array.from, map, filter, and concat create a new array but still reference the same inner objects. Mutating a nested object in one array is visible through the other unless you deep clone.

3. Is `map((x) => x)` the same as copying an array?

It creates a new array and new element slots, but it runs a callback for each index. For sparse arrays, behavior can differ from slice() or spread (holes vs undefined). Prefer spread or slice for a plain shallow copy unless you are transforming values.

4. Do spread and slice behave the same on sparse arrays?

Not always. Spread tends to turn holes into undefined in the new array, while slice can preserve holes. map skips holes but preserves them in the result. Prefer explicit Array.from with a mapper or document the behavior you rely on.
Olorunfemi Akinlua

Boasting over five years of experience in JavaScript, specializing in technical content writing and UX design. With a keen focus on programming languages, he crafts compelling content and designs …