Problem Statement
JavaScript does not provide built-in ways to handle deep-related algorithms. I'd like to discuss what it would look like to bring them into JavaScript. I'm primarily focusing on deep cloning here, but it could be nice to tackled other deep-related algorithms in the future by following the same general philosophy (such as deep comparison, deep traversal, etc).
To preface this
- Yes, I know that implementing a universal deep-clone algorithm isn't possible for many reasons (e.g. private fields).
- Yes, I know structuredClone() is a thing, but it's not part of the core JavaScript spec, nor does it help with userland classes.
One goal I'd like to make as part of this is that I want the target audience of this proposal to be the end-users who need these features, not library authors. I don't want library authors to feel compelled to provide custom implementations of deep algorithms for every class they write (i.e. I don't want this to become like Java's equals()/hashCode() stuff).
A possible deep-clone solution
A new Object.deepClone(value, fallbackBehavior)
function is provided to expose the deep clone functionality. The fallbackBehavior
parameter is optional, and will be explained it further down.
JavaScript, by default, will handle deep cloning of the following data types for you:
- primitives (strings, symbols, etc) - these will be returned as-is
- objects who's prototype is either
Object.prototype
ornull
: Create a new object with the same prototype. For each own property on the source object (including properties with symbol keys) create a property with the same key and a deep clone of the value (using this same deep clone algorithm). Descriptors, such as "writable" and "enumerable", will be preserved. If a getter or setter is encountered, an error will be thrown. - arrays: deep-clone every value in the array. Array holes are preserved.
- sets/maps: Create a new set/map with each entry deep-cloned.
- Any other value: Check if there is a userland deep-cloning algorithm available for the value. If so, use it. If not, throw an error.
(In general, if we don't know how to do something, don't guess, just throw an error. The end-user can always give us more specific instructions via the yet-to-be-explained "fallbackBehavior" parameter).
Userland deep-cloning algorithms can be provided in two ways:
- By defining a
Symbol.deepClone
property on an object. - via the "fallbackBehavior" parameter that you can pass into
Object.deepClone()
.
To align with the goal stated earlier (this is primarily for end-users, not library authors), I want to make sure the end-user has the power to define a deep-cloning algorithm for values they do not own, which is why Object.deepClone()
accepts a fallbackBehavior
parameter. fallbackBehavior
is a function that'll be called anytime the deep-clone algorithm runs across a value that it doesn't know how to clone.
For example, say URLSearchParams
does not, by default, define a deep-clone algorithm (via Symbol.deepClone
), and you have some data that may include URLSearchParams
instances that you wish to deep-clone. In this scenario, you'd be able to define a deep-clone algorithm for URLSearchParams
like this:
// `value` is a value that the deep-clone algorithm does not know how to clone.
// You can call `clone` to deep-clone any child data.
// `clone` is just defined as `value => Object.cloneDeep(value, fallbackBehavior)`
// where `fallbackBehavior` is automatically being supplied - it's set to this function.
// `throwUnclonableError` can be called if you don't know what to do with the value.
// It'll just cause an error to be thrown back to the end-user.
function fallbackCloneBehaviors(value, clone, throwUnclonableError)
if (value instanceof URLSearchParams) {
return new URLSearchParams(
// (I know you don't really need to deep-clone these values - they're just strings, but
// I'm doing it anyways toΒ· demonstrate).
[...value].map(([key, value]) => [key, clone(value)])
)
} else {
throwUnclonableError();
}
]);
Object.deepClone(valueToClone, fallbackCloneBehaviors);
If you have a userland class, and you'd like to supply deep-clone behavior to it, you can do so like this (but do this sparingly - only do this if you have a good reason to suspect that people actually want to deep-clone your class instances).
class MyArray {
#contents = []
...
[Symbol.deepClone](clone) {
return #contents.map(value => clone(value))
}
}
Misc thoughts
Should all built-in classes have the deep-clone algorithm defined on them (including web-API and Node classes)?
No. In fact, most shouldn't. I think this is important to help reinforce the idea that it's primarily the end-user's responsibility to provide the deep-cloning algorithms. But any core, general-purpose data structures (such as Map) should have a deep-cloning algorithm defined on them. Things like Date
or URLSearchParams
I don't have a strong opinion either way, but I'm leaning towards no.