Regarding the author’s chosen title … “Don’t Suck at Cloning JavaScript Objects Anymore” … the author just did exactly that.
The custom recursion based function, praised by the author as "most flexible and reliable solutions" capable of "clon[ing] virtually any object" and advertised as "The most versatile, capable of cloning almost any object with proper implementation.", is doomed to failure.
It already crashes at objects which do not feature the hasOwnProperty
method like with const obj = Object.create(null)
. In addition, due to relying on key in obj
and a direct assignment via copy[key]
, it in fact repeats the approach of structuredClone
but without the latter's rock-solid reliability and true versatility towards object transfers.
Any real deep copy approach which tries to aim reaching beyond the capabilities of structuredClone
needs to be always based on at least the recursive iteration of all of a current object level’s own property-descriptors, regardless of whether the key is string based or it is a symbol based key.
The example code which is going to be provided next was written as a direct response to the author’s original post. And even though it is better and safer than what was offered within the article, I do warn people of using it for any other purposes than educational/self-learning ones. There are rarely any other use cases for deeply copied objects than what structuredClone
does not already cover. And regarding the latter’s support, it can be used.
// a strictly educational deep clone attempt/approach.
function exposeInternalyType(value) {
return Object.prototype.toString.call(value);
}
function isRegExp(value) {
return exposeInternalyType(value) === '[object RegExp]';
}
function isDate(value) {
return exposeInternalyType(value) === '[object Date]';
}
function deepClone(value) {
// assume a primitive type.
let clone = value;
// handle object like types (functions excluded).
if (value && typeof value === 'object') {
if (isRegExp(value)) {
clone = new RegExp(value);
} else if (isDate(value)) {
clone = new Date(value);
} else if (Array.isArray(value)) {
// recursive cloning.
clone = value.map(deepClone);
} else {
clone = Object.create(Object.getPrototypeOf(value));
const entryList = [
...Object.getOwnPropertyNames(value),
...Object.getOwnPropertySymbols(value),
]
.map(key =>
[key, Reflect.getOwnPropertyDescriptor(value, key)]
);
entryList.forEach(([key, descriptor]) => {
const { writable, enumerable, configurable } = descriptor;
const isPublicValue = Object.hasOwn(descriptor, 'value');
if (writable && enumerable && configurable && isPublicValue) {
// recursive cloning.
// // clone[key] = deepClone(value[key]);
clone[key] = deepClone(descriptor.value);
} else if (isPublicValue) {
// recursive cloning.
// // Reflect.defineProperty(clone, key, {
// // descriptor, value: deepClone(value[key]),
// // });
Reflect.defineProperty(clone, key, {
descriptor, value: deepClone(descriptor.value),
});
} else {
// assume at least a read-only, `get` protected, value.
// recursive cloning.
Reflect.defineProperty(clone, key, {
descriptor, value: deepClone(value[key]),
});
// or any other handling of how to clone a protected value.
}
});
}
} else if (typeof value === 'function') {
// handle function objects.
clone = Function.prototype.bind.call(value);
// or any other handling of how to (not) clone a function.
}
return clone;
}