[JS] Shallow Copy v.s. Deep Copy

[JS] Shallow Copy v.s. Deep Copy

What You Can Expect from This Post

Recently, I was building a small browser game in JavaScript.

The game had a board — just a simple array that changed every round depending on player actions. At the end of the game, I wanted to reset the board to its initial state so players could play again.

Sounds simple, right?

But I ran into a problem: when I thought I had "copied" the original board and used it to reset the game... the board wasn’t resetting at all!

The "original" state had already been changed somewhere along the way. That’s when I realized: I hadn’t copied the board — I had mutated it. To fix it, I needed a deep copy, not just a shallow one.

So in this post, I want to help you avoid the same mistake.


A Real-World Bug from My Game

Let me show you what I did wrong.

const initialBoard = [
  ["", "", ""],
  ["", "", ""],
  ["", "", ""],
];

let currentBoard = initialBoard; // I *thought* this was a new board

currentBoard[0][0] = "X"; // Player makes a move
currentBoard[1][1] = "O";

// Game ends – I try to reset the board
currentBoard = initialBoard;

console.log(currentBoard);
// Wait... it's already been modified!

At first glance, I thought I had a clean reset — just reassign currentBoard = initialBoard.But both variables were pointing to the same array in memory. Any changes I made to currentBoard also affected initialBoard .


Why This Happens: Reference Types in JavaScript

In JavaScript, arrays and objects are reference types. That means when you assign one variable to another, you’re not making a new copy — you’re creating a new reference to the same underlying data. For more details about reference type and primitive types, you can refer to the following post. https://academind.com/tutorials/reference-vs-primitive-values

const a = [1, 2, 3];
const b = a;

b[0] = 99;

console.log(a); // [99, 2, 3] 

Shallow Copy: Only the Top Layer

I then thought I could use the spread operator to make a copy:

const currentBoard = [...initialBoard];

But that still didn’t solve the problem — the bug was still there.

Why? Because this only made a shallow copy.

Each row in the board is still an inner array — a reference — and those references were shared.

const shallow = [...initialBoard];
shallow[0][0] = "X";

console.log(initialBoard[0][0]); // "X" still modified

The Right Way: Deep Copy

To truly reset the board, I needed to make a deep copy — meaning I needed to duplicate every nested array (and object, if there were any).

Here’s how I fixed it:

const currentBoard = structuredClone(initialBoard);

currentBoard[0][0] = "X";

console.log(initialBoard[0][0]); // Still ""

Now, initialBoard stays untouched no matter what I do to currentBoard.

Since my game board is a 2D nested array, I can also create a deep copy using the following approach:

const currentBoard = [...initialBoard.map((row) => [...row])];

Conclusion

That little bug in my game taught me a big lesson:

If you copy a reference type the wrong way, you're not copying it — you're still connected to it.

Whenever you're working with arrays or objects — especially for initial states, backup data, or anything that needs to stay clean and untouched — always ask yourself:

  • Am I making a shallow copy?
  • Or do I really need a deep copy?

Take it from me — you don’t want to find out after the boss battle that your “saved game” wasn’t saved at all!