Native deep-clone algorithm?

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 or null: 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.

Related links:

1 Like

Ah, good to know - so it looks like tc39 is taking over the algorithm itself, but not the "structuredClone" global, at least at this time.

1 Like

There is strong opposition to structured cloning as currently specified being in the core language. The main problem is that it pierces into some objects internal fields.

I have been wanting to propose an extensible cloning API for a while that allows user objects to have their internal state cloned. However I don't believe a single fallback callback at the point of cloning is the right approach as it's not composable: a library calling clone internally with user provided objects would need to be somehow configured with the serialization and deserialization logic for these objects.

Instead we likely need a mechanism to "register" handlers for certain objects. The complexity there is how to provide such a mechanism without introducing some observable global mutable state, and how to make this mechanism friendly to multiple realms / agents (it would be nice to postMessage and receive user cloned objects transparently.

3 Likes