Have you ever copied an object in JavaScript, changed a value in the copy, and then realized with horror that the original object changed too?
If you have spent any time building web applications, you have likely encountered this bug. It is a classic "gotcha" that stems from misunderstanding how JavaScript handles data in memory. You think you created a brand new duplicate, but actually, you just created a new label for the old data.
This guide will demystify the concepts of Shallow Copy and Deep Copy. We will break down exactly how JavaScript stores variables, why your copies are linked to the originals, and the modern techniques used to create truly independent duplicates.
The Root of the Confusion: Reference vs. Value
To understand copying, you first need to understand how JavaScript stores data. Not all variables are created equal. In JavaScript, data types fall into two categories: Primitives and Reference Types.
Primitive Types (Passed by Value)
Primitives are simple data types: string, number, boolean, undefined, null, and symbol.
When you assign a primitive value to a variable, the variable stores the actual value directly in a memory space called the "stack." If you copy it, you create a completely independent value.
1let a = 10;
2let b = a; // b gets a copy of the value 10
3b = 20;
4
5console.log(a); // 10 (Unchanged)
6console.log(b); // 20Reference Types (Passed by Reference)
Objects, Arrays, and Functions are reference types. They are more complex and are stored in a memory space called the "heap."
When you assign an object to a variable, the variable does not store the object itself. Instead, it stores a reference (or a memory address) that points to where that object lives in the heap.
Think of the variable as a piece of paper with a home address written on it. The object is the house. If you copy the variable, you are just copying the piece of paper with the address. You now have two pieces of paper, but they both point to the same house.
1let user1 = { name: "Alice" };
2let user2 = user1; // Copies the REFERENCE, not the object
3
4user2.name = "Bob";
5
6console.log(user1.name); // "Bob" - The original changed!This behavior is why we need specific strategies for copying objects.
What is a Shallow Copy?
A shallow copy creates a new object, but it does not create copies of nested objects. It copies the top-level properties only.
If your object is simple (flat) and contains only primitives (like strings and numbers), a shallow copy is perfect. It is fast and efficient. However, if your object contains other objects (like an array inside an object), the shallow copy will only copy the reference to that inner object.
How to Create a Shallow Copy
There are two common modern ways to create a shallow copy in JavaScript.
1. The Spread Operator (...)
Introduced in ES6, the spread operator is the most concise way to copy arrays or objects.
1const original = { name: "John", age: 30 };
2const copy = { ...original };
3
4copy.name = "Jane";
5
6console.log(original.name); // "John" (Safe)2. Object.assign()
This is the older method but works similarly. It merges properties from a source object into a target object.
1const original = { name: "John", age: 30 };
2const copy = Object.assign({}, original);The "Trap" of Shallow Copies
Here is where developers get into trouble. If you use the spread operator on a nested object, the inner data is still linked.
1const original = {
2 name: "John",
3 details: {
4 city: "New York",
5 active: true
6 }
7};
8
9const shallowCopy = { ...original };
10
11// Let's change the city in the copy
12shallowCopy.details.city = "Los Angeles";
13
14// Check the original
15console.log(original.details.city);
16// Output: "Los Angeles" (OOPS! It changed.)Because details is an object, the shallow copy only copied the reference to details. Both original and shallowCopy are pointing to the exact same details object in memory.
What is a Deep Copy?
A deep copy creates a completely independent clone of the original object. It recursively copies every value. If it finds a nested object, it copies that too, rather than just copying the reference.
When you modify a deep copy, you are 100% guaranteed not to affect the original object, no matter how deeply nested the data is.
How to Create a Deep Copy
Historically, this was hard to do natively in JavaScript. We often had to rely on external libraries. However, the landscape has changed recently.
1. structuredClone() (The Modern Standard)
As of 2022, modern browsers and Node.js support a native function called structuredClone(). This is the best way to deep copy objects today.
It handles complex data types like Date, Map, Set, and even circular references (where an object references itself).
1const original = {
2 name: "John",
3 details: { city: "New York" }
4};
5
6// Create a deep copy
7const deepCopy = structuredClone(original);
8
9deepCopy.details.city = "Los Angeles";
10
11console.log(original.details.city);
12// Output: "New York" (Success! The original is safe.)2. JSON.parse(JSON.stringify()) (The "Hack")
Before structuredClone, this was the most common workaround. You convert the object into a JSON string, and then parse it back into a new object.
1const original = { name: "John", data: { id: 1 } };
2const deepCopy = JSON.parse(JSON.stringify(original));Warning: This method has severe limitations.
- It deletes undefined values.
- It converts Date objects into strings.
- It fails completely if your object contains functions.
- It throws an error on circular references.
3. Libraries (Lodash)
If you are working on a legacy codebase or need to support very old browsers, using a utility library like Lodash is a safe bet. The _.cloneDeep() function is the gold standard for robust deep copying.
1import _ from 'lodash';
2
3const deepCopy = _.cloneDeep(original);Comparison: When to Use Which?
Choosing between shallow and deep copies depends on your performance needs and data structure.
Use Shallow Copy When:
- Performance is critical: Shallow copying is extremely fast because it only copies the first layer of data.
- Data is flat: Your object has no nested objects or arrays (e.g., { id: 1, status: 'active' }).
- Immutability patterns (Redux/React): often, you only need to update one specific level of the state tree, and shallow copies are preferred to avoid unnecessary processing.
Use Deep Copy When:
- You have complex, nested state: Your data looks like a tree structure.
- Total isolation is required: You are passing data to a function that might mutate it, and you need to ensure your local version remains untouched.
- Preventing side effects: You are working with global store data and want to edit a "draft" version before saving.
Performance Considerations
It is important to note that deep copying is expensive.
Imagine you have a large dataset with 10,000 rows, and each row has nested arrays. If you run a deep copy every time a user types a character in a search box, your application will freeze. The computer has to visit every single property in that massive tree and allocate new memory for it.
Pro Tip: purely immutable libraries like Immer.js or Immutable.js offer a middle ground. They use a technique called "Structural Sharing." When you change a nested property, they only copy the parts of the tree that changed and reuse the references for the parts that didn't. This gives you the safety of a deep copy with the performance of a shallow copy.
Common Edge Cases and Gotchas
Even with structuredClone, copying in JavaScript can be tricky. Here are specific scenarios to watch out for.
1. Copying Functions
Neither JSON.stringify nor structuredClone can copy functions. Functions in JavaScript are complex objects tied to their scope (closures). If you try to deep copy an object containing a method, the method will often be lost or throw an error.
If you need to copy functions, you usually have to manually assign them or rethink your architecture to separate data (JSON) from behavior (functions).
2. Circular References
A circular reference happens when an object refers to itself.
1const user = { name: "Alice" };
2user.self = user; // Circular reference- JSON.stringify(user) will crash your app with a "Converting circular structure to JSON" error.
- structuredClone(user) handles this perfectly. It preserves the circular reference in the copy.
3. Date and RegExp Objects
If you use the JSON hack, a Date object turns into a string like 2023-10-25T10:00:00.000Z. When you parse it back, it remains a string. You lose the ability to call methods like .getFullYear().
structuredClone preserves the Date object type correctly.
FAQ
Q: Is the spread operator ... a deep copy or shallow copy? A: The spread operator creates a shallow copy. It copies the top-level properties. If those properties are references to other objects, those references are shared between the copy and the original.
Q: Why shouldn't I just use deep copy everywhere to be safe? A: Speed and memory usage. Deep copying doubles the memory footprint for that data and takes CPU cycles to traverse the entire object tree. In high-performance apps (like games or real-time dashboards), this lag is noticeable.
Q: Does assigning a variable (let a = b) create a shallow copy? A: No. Assigning a variable creates no copy at all for objects. It simply creates a new reference pointing to the exact same object in memory. A shallow copy creates a new object structure, even if the inner contents are shared.
Q: Can structuredClone copy DOM elements? A: No. structuredClone is designed for data objects (JavaScript values). It cannot clone HTML elements or DOM nodes. For that, you should use the DOM method node.cloneNode(true).
Conclusion
Understanding the difference between shallow and deep copies is a milestone in a JavaScript developer's journey. It marks the transition from "making it work" to "understanding how it works."
Here is the quick summary:
- Assignment (=): No copy. Two variables point to the same object.
- Shallow Copy (... or Object.assign): New outer shell, shared inner contents. Fast, but risky for nested data.
- Deep Copy (structuredClone): Brand new independent object. Safe, but slower.
For most modern web development, default to using the spread operator for simple updates. When dealing with complex state or nested data that must remain isolated, reach for structuredClone(). By choosing the right tool for the job, you ensure your applications are both performant and bug-free.
About the Author

Suraj - Writer Dock
Passionate writer and developer sharing insights on the latest tech trends. loves building clean, accessible web applications.
