
To explain shallow copy and deep copy clearly, it helps to first understand JavaScript data types.

Except for Object, all types define immutable values at the lowest level of the language. We call these values primitive values.
In computer science, an object is a region of memory that can be referenced by an identifier. In JavaScript, objects are the main mutable values. In fact, functions are also objects, just with callable behavior.
Primitive values are typically discussed as being stored directly, while reference-type data such as objects is stored by reference. A variable holds either a direct primitive value or a reference to a location where the object data lives. Once that distinction is clear, the difference between shallow copy and deep copy becomes much easier to understand.
When we talk about shallow and deep copy, we are mainly talking about objects. Primitive values are simply copied as values, so the deep-vs-shallow distinction is not especially meaningful for them.
A shallow copy of an object copies the object's top-level properties, but if a property points to another object, the copy and the original still share the same reference.
That means:
The core behavior is that only one layer is copied. If a property is a primitive, the copied value is independent. If a property is a reference type, the copied result still points to the same underlying object.
Important: changing a nested property's contents is very different from assigning an entirely new value to that property. That distinction matters a lot when thinking about shallow copies.
A deep copy creates a completely independent copy of the source object's nested structure.
That means:
In short, deep copy clones the object together with the objects it refers to.
Object.assign const objClone = Object.assign(target, ...sources);Symbol properties// object
const objClone = { ...obj };
// array
const arrClone = [...arr];Its limitations are similar to Object.assign(), but it is often more convenient when the data is mostly primitive fields.
slice, concat, and Array.from // slice
const arrCloneBySlice = arr.slice();
// concat
const arrCloneByConcat = [].concat(arr);
// Array.from
const arrCloneByArrayFrom = Array.from(arr);function clone(target) {
if (typeof target === "object" && target !== null) {
const cloneTarget = Array.isArray(target) ? [] : {};
for (let prop in target) {
cloneTarget[prop] = target[prop];
}
return cloneTarget;
} else {
return target;
}
}JSON.stringify() function deepClone(target) {
if (typeof target === "object" && target !== null) {
return JSON.parse(JSON.stringify(target));
} else {
return target;
}
}This is the simplest and most direct approach. It works for some basic cases, but it has many limitations. If your data is simple and you understand the trade-offs, it may be enough.
lodash.cloneDeep() and jQuery.extend() These are representative utility-library solutions and are straightforward to use.
// lodash.cloneDeep()
_.cloneDeep(value);
// jQuery.extend()
jQuery.extend(true, {}, value);Before implementing your own deep clone, ask what it needs to support:
Symbol keys?Date, RegExp, Function, Map, and Set?You can improve the implementation step by step.
Turn the earlier shallow copy into a recursive version so it can copy deeper levels:
function deepClone(target) {
if (typeof target === "object" && target !== null) {
const cloneTarget = Array.isArray(target) ? [] : {};
for (let prop in target) {
if (typeof target[prop] === "object" && target[prop] !== null) {
cloneTarget[prop] = deepClone(target[prop]);
} else {
cloneTarget[prop] = target[prop];
}
}
} else {
return target;
}
}
This first version only really satisfies the first two conditions. To support the rest, the function needs to become more complete:
function deepClone(target) {
const map = new WeakMap();
function isObject(data) {
return (typeof data === "object" && data) || typeof data === "function";
}
function clone(data) {
if (!isObject(data)) {
return data;
}
if ([Date, RegExp].includes(data.constructor)) {
return new data.constructor(data);
}
if (typeof data === "function") {
return new Function("return " + data.toString())();
}
const exist = map.get(data);
if (exist) {
return exist;
}
if (data instanceof Map) {
const result = new Map();
map.set(data, result);
data.forEach((val, key) => {
if (isObject(val)) {
result.set(key, clone(val));
} else {
result.set(key, val);
}
});
return result;
}
if (data instanceof Set) {
const result = new Set();
map.set(data, result);
data.forEach((val) => {
if (isObject(val)) {
result.add(clone(val));
} else {
result.add(val);
}
});
return result;
}
const keys = Reflect.ownKeys(data);
const allDesc = Object.getOwnPropertyDescriptors(data);
const result = Object.create(Object.getPrototypeOf(data), allDesc);
map.set(data, result);
keys.forEach((key) => {
result[key] = isObject(data[key]) ? clone(data[key]) : data[key];
});
return result;
}
return clone(target);
}This version handles a much wider set of cases:
WeakMapDate and RegExpMap and SetShallow copy and deep copy are fundamentally about whether nested references are shared.
In simple cases, spread syntax or Object.assign() is usually enough for shallow copy. For deep copy, JSON.parse(JSON.stringify(...)) is only safe for limited data shapes. In real projects, structuredClone, lodash.cloneDeep, or a carefully designed custom solution is usually the better choice when the data structure is more complex.