AI Computer Institute
Expert-curated CS & AI curriculum aligned to CBSE standards. A bharath.ai initiative. About Us

The Spread Operator (...): Copy, Merge, and Expand

📚 JavaScript Advanced⏱️ 22 min read🎓 Grade 9
✍️ AI Computer Institute Editorial Team Updated: August 2026 CBSE-aligned · Peer-reviewed · 22 min read
Content curated by subject matter experts with IIT/NIT backgrounds. All chapters are fact-checked against official CBSE/NCERT syllabi.

Suppose you and your classmate Priyanka are both preparing the same class 9-B attendance register for a school trip. She hands you her list of names so you can add three more students who joined late. You do not want to write on her original sheet, so you take a photocopy, add your three names to the photocopy, and hand the original back untouched. That is what "copying" means to a human being — two separate pieces of paper, and writing on one never changes the other.

Now watch what happens when a beginner tries to do the same thing in JavaScript, expecting arrays to behave like photocopies.

let originalList = ["Aarav", "Diya", "Kabir"];
let assignedCopy = originalList;

assignedCopy.push("Meera");

console.log(originalList);
console.log(assignedCopy);

A student who thinks like the photocopy story expects originalList to still read ["Aarav", "Diya", "Kabir"], since only assignedCopy was changed. But the actual output is:

["Aarav", "Diya", "Kabir", "Meera"]
["Aarav", "Diya", "Kabir", "Meera"]

Both variables changed. This is not a JavaScript bug — it is JavaScript telling you something precise about how arrays are stored in memory, and it is exactly the problem the spread operator (three dots, written ...) was built to solve. By the end of this chapter you will know exactly why the code above behaves this way, how to write a line that produces a true, independent copy, and how the same three-dot symbol lets you merge lists, combine records, and expand data into function calls.

What "=" Actually Copies for Arrays and Objects

To understand the bug, forget photocopies for a moment and think about a school locker room. Each locker has contents, and each locker has a locker number written on a small tag. Now imagine your variable name is not the locker itself — it is the tag. When you write let originalList = ["Aarav", "Diya", "Kabir"], JavaScript does two things: it creates an array object somewhere in memory (say, at address #101), and it makes the tag originalList point to that address.

When you then write let assignedCopy = originalList, JavaScript does not open locker #101, take out the contents, and put them into a brand-new locker. It simply copies the address written on the tag. Now you have two tags, originalList and assignedCopy, both pointing at the exact same locker, #101. There is still only one array in memory. When you call assignedCopy.push("Meera"), you are not modifying the tag — you are walking up to locker #101 and adding an item to it. Since originalList points to that same locker, it "sees" the new item too, because there was never a second locker to begin with.

This behavior is called copying by reference, and it applies to arrays, objects, functions, and every other non-primitive value in JavaScript. It is different from how primitive values — numbers, strings, booleans — behave. If you write let a = 5; let b = a; b = b + 1;, then a stays 5, because numbers are copied by value: b gets its own independent number, not an address pointing back to a. Arrays and objects do not get this courtesy — = only ever copies the address, never the contents. Nothing is broken here; this is a deliberate design choice that makes JavaScript fast, because copying a locker's address takes one step, while copying every item inside a 10,000-element array would take 10,000 steps every single time you wrote =.

The problem is that this efficient default is exactly wrong when what you actually want is Priyanka's photocopy — a second, independent locker with its own copy of the same starting contents. That is the job the spread operator does.

The Spread Operator: Unpacking a Collection

The spread operator is written as three dots, ..., placed directly before an array, an object, or any other iterable value. Read it as the instruction "take this collection and unpack every element out of it, one by one, right here." It does not create the three-dot symbol as a value itself — it only makes sense when it appears inside something that is collecting values, such as a new array literal [ ], a new object literal { }, or the parentheses of a function call.

Go back to the locker analogy. [...originalList] means: walk to locker #101, take out every item one at a time — "Aarav", then "Diya", then "Kabir" — and place each item, individually, into a fresh, brand-new locker that the square brackets [ ] are creating right now. The new locker gets a new address, say #205. Nobody ever writes the address #101 onto the new locker's tag. The two lockers now hold identical starting contents, but they are physically separate, exactly like Priyanka's photocopy.

Here is the same three-variable scenario from the opening example, this time using spread for the second copy:

let originalList = ["Aarav", "Diya", "Kabir"];
let assignedCopy = originalList;      // same address as originalList
let spreadCopy = [...originalList];   // brand-new address

assignedCopy.push("Meera");

console.log(originalList);  // ["Aarav", "Diya", "Kabir", "Meera"]
console.log(assignedCopy);  // ["Aarav", "Diya", "Kabir", "Meera"]
console.log(spreadCopy);    // ["Aarav", "Diya", "Kabir"]

Trace it exactly as JavaScript would. Line 1 creates array #101 with three names and points originalList at it. Line 2 copies the address #101 into assignedCopy — no new array exists yet. Line 3 evaluates [...originalList]: it opens #101, reads out "Aarav", "Diya", "Kabir" in order, and builds a new array at, say, #205, then points spreadCopy at #205. Line 5 pushes "Meera" onto whatever assignedCopy points to — which is #101 — so array #101 becomes four elements long. Array #205 was never touched by that push, so spreadCopy still shows exactly three names. Notice also that originalList and assignedCopy report the identical, updated four-name list — proof that they were always the same locker wearing two different tags.

The diagram below shows the same trace visually: on the left, assignment makes two tags share one address; on the right, spread reads out the contents and builds a second, independent address.

Assignment ( = ) assignedCopy = originalList Memory Address #101 ["Aarav","Diya","Kabir"] originalList assignedCopy Both tags point to the SAME address assignedCopy.push("Meera") changes BOTH variables Spread ( ... ) spreadCopy = [...originalList] Memory Address #101 ["Aarav","Diya","Kabir"] unpack & copy Memory Address #205 (NEW) ["Aarav","Diya","Kabir"] spreadCopy A brand-new, independent address originalList.push("Meera") does NOT affect spreadCopy

Merging Two Arrays with Spread

Once you can picture spread as "unpack the contents here," merging two arrays becomes a one-line operation. Suppose your school keeps two separate arrays for the two sections of class 9 that are running in the same relay race, and the sports teacher needs one combined list:

let section9A = ["Rohan", "Ishita"];
let section9B = ["Vivaan", "Ananya"];

let allRunners = [...section9A, ...section9B];

console.log(allRunners);
// ["Rohan", "Ishita", "Vivaan", "Ananya"]

Read the right-hand side left to right, exactly as JavaScript evaluates it: the outer [ ] starts building a new array. ...section9A unpacks "Rohan" and "Ishita" into it first. Then ...section9B unpacks "Vivaan" and "Ananya" right after. The final array is a new, fourth locker containing all four names in the order the spreads appeared — neither section9A nor section9B is modified or even touched beyond being read.

Because spread just places values wherever it is written, you are not restricted to only spreading at the start. You can mix spread with individual values, and position matters exactly the way it looks:

let topper = "Priyanka";
let withTopperFirst = [topper, ...allRunners];

console.log(withTopperFirst);
// ["Priyanka", "Rohan", "Ishita", "Vivaan", "Ananya"]

Before spread existed, JavaScript programmers merged arrays using section9A.concat(section9B), and inserted a value at the front using array.unshift(topper) combined with copying tricks. Spread does not replace these methods, but it reads closer to plain English and — critically for this chapter's theme — it always produces a fresh array rather than modifying an existing one, so it never causes the "two tags, one locker" surprise from the previous section.

Spreading an Array Into a Function Call

Spread is not limited to array literals. It also works inside the parentheses of a function call, where it unpacks an array into separate, individual arguments. Consider a function that Indian school report cards use constantly: finding the highest mark among several subjects.

let marks = [78, 92, 85, 67, 99];

let highest = Math.max(...marks);

console.log(highest);  // 99

Math.max does not accept a single array as its argument — it expects each number handed to it separately, like Math.max(78, 92, 85, 67, 99). Writing Math.max(marks) without the dots would actually fail to give a sensible number, because Math.max would receive one argument that is an entire array, not five numbers, and it cannot compare an array to anything meaningfully. The three dots solve this by unpacking the array into exactly the five separate arguments Math.max was built to accept, before the function call even happens. JavaScript performs the unpacking first, then calls Math.max(78, 92, 85, 67, 99), which correctly returns 99.

Do Not Confuse Spread With Rest — Same Dots, Opposite Direction

Grade 9 learners frequently meet a second use of three dots, called the rest parameter, and mix it up with spread because the symbol looks identical. The direction of the operation is exactly reversed, so it helps to memorize a single rule: spread unpacks a collection into separate values; rest gathers separate values back into a collection.

function totalMarks(...scores) {
  let sum = 0;
  for (let i = 0; i < scores.length; i++) {
    sum = sum + scores[i];
  }
  return sum;
}

console.log(totalMarks(78, 92, 85));      // 255
console.log(totalMarks(...marks));        // 421

Inside the function's parameter list, ...scores is rest: it takes however many separate arguments were passed in and gathers them into one array called scores. In the first call, three separate numbers are gathered into scores = [78, 92, 85], and the loop adds them to get 255. In the second call, ...marks at the call site is spread: it unpacks the five-element array marks into five separate arguments first, which the function's rest parameter then re-gathers into scores = [78, 92, 85, 67, 99], summing to 421 (78+92+85+67+99). The dots look the same; whether you are inside a function call (spread, unpacking) or inside a function's own parameter list (rest, gathering) tells you which direction is happening.

Spread Works on Any Iterable — Not Just Arrays

The spread operator is not limited to arrays. It works on any "iterable" — any value JavaScript knows how to step through one element at a time. Strings are iterable, stepping through one character at a time:

let pnr = "PNR12345";
let characters = [...pnr];

console.log(characters);
// ["P","N","R","1","2","3","4","5"]

A Set — a built-in JavaScript collection that automatically stores only unique values — is also iterable, which makes spread a natural way to deduplicate an array. Suppose an attendance scanner accidentally logs a roll number twice because a student swiped their card twice:

let rollNumbers = [12, 5, 12, 8, 5, 21];
let uniqueRollNumbers = [...new Set(rollNumbers)];

console.log(uniqueRollNumbers);
// [12, 5, 8, 21]

new Set(rollNumbers) builds a Set that keeps only the first occurrence of each number, in the order it first appeared: 12, then 5, then 8, then 21 (the second 12 and second 5 are silently dropped because a Set never stores duplicates). That Set is not an array, so it does not have array methods like push or map — spreading it with [...] converts it back into a plain array you can use normally.

The Object Spread: Copying and Updating Records

Since 2018, the same three-dot syntax also works inside object literals { }, unpacking an object's key-value pairs instead of an array's elements. This is extremely useful when you want to create an updated version of a record without disturbing the original — for example, promoting a student to the next grade while keeping every other field the same:

let student = { name: "Ira", grade: 9, city: "Pune" };
let updatedStudent = { ...student, grade: 10 };

console.log(updatedStudent);
// { name: "Ira", grade: 10, city: "Pune" }

console.log(student);
// { name: "Ira", grade: 9, city: "Pune" }   -- unchanged

Read the right-hand side of line 2 in order, because order decides the result: ...student first unpacks all three key-value pairs — name: "Ira", grade: 9, city: "Pune" — into the new object. Then grade: 10, written after the spread, is applied on top and overwrites the grade that spread had just placed there. If a key is written more than once while an object literal is being built, the last value assigned to that key wins, exactly like writing the same line twice in a notebook — only the final version remains. Because {...student} builds a brand-new object at a new address, exactly like array spread did, student itself is completely untouched.

Common Misconception: Spread Is a Shallow Copy, Not a Deep Copy

This is the single most important warning in this chapter, and it is where even confident students get caught out. Spread does solve the "two tags, one locker" problem — but only for the outer layer of the collection. If any value inside the array or object is itself another array or object, spread copies the reference to that inner collection, not a fresh copy of its contents. This is called a shallow copy, as opposed to a deep copy, which would recursively copy every nested level too. Spread only ever performs a shallow copy.

Watch what goes wrong when a nested array is involved:

let classInfo = {
  section: "9A",
  toppers: ["Aarav", "Diya"]
};

let classCopy = { ...classInfo };

classCopy.section = "9B";           // safe
classCopy.toppers.push("Kabir");    // NOT safe

console.log(classInfo.section);     // "9A"  (unaffected — correct!)
console.log(classInfo.toppers);
// ["Aarav", "Diya", "Kabir"]  -- changed, even though we never touched classInfo directly!

classCopy.section = "9B" behaves exactly as expected, because section holds a primitive string, and reassigning classCopy.section simply points that one key at a new string — it never reaches back to touch classInfo. But toppers holds an array, and when { ...classInfo } unpacked the toppers key, it copied the address of that array, not a new array. So classInfo.toppers and classCopy.toppers are, right now, two tags on the very same locker — precisely the situation spread was supposed to prevent, just one level deeper than spread actually looks. Calling .push("Kabir") on that shared array changes it for both variables, because there is still only one toppers array in memory.

The correct mental rule: spread copies one level deep. Top-level primitive values (numbers, strings, booleans) become genuinely independent. Top-level arrays and objects become independent containers, but anything nested a second level inside — an array inside an object, an object inside an array — is still shared by reference. If you need a true deep copy of nested data, spread alone is not enough; you would need to spread each nested level separately, or use a dedicated deep-copy technique, which is a topic for a later chapter.

Check Your Understanding

Work out each answer by tracing the code line by line before checking, the same way you traced the opening example.

  1. Predict the output.

    let fruits = ["mango", "guava"];
    let copy1 = fruits;
    let copy2 = [...fruits];
    copy1.push("litchi");
    copy2.push("banana");
    console.log(fruits.length, copy2.length);
    

    Answer: 3 3. copy1 shares fruits's address, so pushing "litchi" through copy1 makes fruits three elements long. copy2 is an independent array that started with 2 elements and had "banana" pushed onto it separately, also reaching 3 — the two 3s are a coincidence of counting, not evidence that the arrays are the same object.

  2. Predict the output.

    let a = [1, 2];
    let b = [3, 4];
    console.log([...b, ...a]);
    

    Answer: [3, 4, 1, 2]. Spread places values in the exact order they are written in the array literal — b's elements first, then a's.

  3. Predict the output.

    let record = { city: "Chennai", pin: 600001 };
    let updated = { ...record, pin: 600028, area: "T. Nagar" };
    console.log(Object.keys(updated).length);
    

    Answer: 3. updated ends up with city, pin, and area — the spread brought in city and pin, the explicit pin: 600028 overwrote the spread's pin value (not added a new key), and area is a genuinely new key.

  4. Spot the bug. A student writes this to give every student in an array a bonus mark, without changing the original array:

    let students = [
      { name: "Zoya", marks: [80, 85] }
    ];
    let studentsCopy = [...students];
    studentsCopy[0].marks.push(100);
    

    Will students[0].marks also gain the 100? Answer: Yes. [...students] only copies the outer array — it creates a new array holding references to the same student objects, not new copies of them. studentsCopy[0] and students[0] point to the identical object, so .marks.push(100) changes marks that both variables can see. This is the shallow-copy trap from the previous section, appearing with objects nested inside an array instead of arrays nested inside an object.

  5. Conceptual. Why does Math.max(marks) (without spread) not work correctly when marks is an array of numbers, while Math.max(...marks) does?

    Answer: Math.max is written to compare however many separate number arguments you give it — it has no special logic to look inside a single array argument. Math.max(marks) passes one argument (the whole array), which Math.max cannot meaningfully compare against nothing else, so it returns NaN. Math.max(...marks) unpacks the array into separate numeric arguments before the call happens, so Math.max receives exactly what it expects.

Summary

  • = on an array or object copies only the address in memory, not the contents — two variables end up pointing at one shared collection, and a change through either one is visible through both.
  • The spread operator, ..., unpacks every element of an array, object, or other iterable at the exact position it is written, letting you build a genuinely new, independent collection.
  • [...originalArray] makes a true top-level copy; [...arr1, ...arr2] merges arrays in the order written; a plain value mixed with spreads, like [first, ...rest], is inserted exactly where it is placed.
  • Spread inside a function call, like Math.max(...marks), unpacks an array into separate arguments, because functions like Math.max expect individual values, not one array.
  • Rest parameters, written ...name inside a function's own parameter list, do the opposite job: they gather separate incoming arguments into one array. Same symbol, opposite direction — spread unpacks, rest gathers.
  • Spread works on any iterable, including strings (character by character) and Sets (useful for removing duplicates from an array via [...new Set(array)]).
  • Object spread, { ...obj }, copies key-value pairs; a key written again after the spread overwrites the value the spread had just placed there, which is how you create an "updated" copy of a record.
  • Spread is a shallow copy only. It makes the outer array or object independent, but any array or object nested inside is still shared by reference with the original. Mutating a nested collection through the copy will change the original too — always check whether your data has a second layer before assuming spread has fully protected it.

Practice Exercises

Now it is time to practice! Complete these challenges to solidify your understanding:

  • Exercise 1: Write a short program that demonstrates the core concept from this chapter. Test it with at least 3 different inputs.
  • Exercise 2: Find a real-world example where the spread operator (...): copy, merge, and expand is used in an Indian company (like TCS, Infosys, Flipkart, or ISRO). Write a paragraph explaining the connection.
  • Exercise 3: Create a mind-map connecting the spread operator (...): copy, merge, and expand to at least 3 other topics you have studied.
← Async/Await: Writing Asynchronous CodeEvent Delegation: Efficient DOM Event Handling →

Found this useful? Share it!

📱 WhatsApp 🐦 Twitter 💼 LinkedIn