A deep cloning algorithm aware of prototypes, getters/setters, etc.


Keywords
deep, clone, copy, recursive, prototype, getter, setter, javascript
License
ISC
Install
npm install true-clone@1.0.0

Documentation

js-true-clone

The goal of this package is to get as close as possible to a perfect JS value clone.

Usage

npm i true-clone

then

const { clone } = require('true-clone');
// later ...
const cloned = clone(myObject);

Behaviour

The cloning algorithm is pretty smart and is aware of:

  • Native JS types! This includes primitives, Array, Set, Map, boxed primitives, typed arrays, etc.
  • Prototypes! Finally, you can clone custom classes!
  • Getters! These will be replicated on the result as getters, not as the computed value.
  • Setters! These will be replicated on the result.
  • Custom properties on native types! For instance: const ar = []; ar.my = 'prop'; console.assert(clone(ar).my === 'prop').
  • (Non-)enumerability, (non-)configurability, and/or (non-)writability of object properties! These will be respected.
  • etc.

Details

  • Mostly works as one would expect!
  • However, the following may be notable:
  • Prototypes: are referenced rather than copied; Object.is(clone(Object.create(someProto)).prototype, someProto)
  • Proxy objects: do not return other proxies. Additonally, all traps are ignored besides the following:
    • getPrototypeOf: given prototype is assigned to new object
    • ownKeys: these are the keys that will appear on the clone
    • getOwnPropertyDescriptor: is used to define properties on the clone
  • Due to JS limitations, objects of the type Function, WeakSet, and WeakMap will not be cloned and will instead be returned as-is.

Comparison

Suite in tests.js run on different packages using node v14.2.0. See compare.sh.

package \ feature primitives native types prototypes monkeypatching relations rich properites
true-clone 1.0.0 s s s s s s
clone 2.1.2 s p 1 s p 1 s u
lodash.clonedeep 4.5.0 s p 2 s p 3 p 4 u
rfdc 1.1.4 s p 5 u u p 6 u
Key

s: all tests passing; u: no tests passing; p: some tests passing

  • primitives: supports primitive values
  • native types: supports certain native types such as Array and Set
  • prototypes: supports objects with prototypes
  • monkeypatching: copies over monkeypatched attributes
    • e.g. const ar = []; ar.my = 'prop'; console.assert(clone(ar).my === 'prop')
  • relations: preserves relational identity, such as in cyclic and diamond-shaped structures
    • cyclic e.g. e.g. const ar = []; ar.push(ar);
    • diamonds e.g. const child = { i_am: 'child' }; const parent = { child_a: child, child_b: child };
  • rich properties: getters and setters etc.
Details
  • 1: fails for Number, String, ArrayBuffer, DataView, errors types, and typed arrays.
  • 2: fails for sparse arrays, BigInt64Array, BigUint64Array, and error types
  • 3: fails for Array, BigInt64Array, BigUint64Array, and error types
  • 4: fails for cyclic Map and Set objects
  • 5: fails for Number, String, Boolean, RegExp, Map, Set, ArrayBuffer, DataView, typed arrays, and error types.
  • 6: fails for diamond shapes and cyclic non-Object values

Benchmarks

true-clone pays for its correctness with speed. Benchmark is run on my personal machine; they should be considered only in relation to each other. See benchmark.js.

package \ scope primitives native object types plain objects arrays
true-clone 1.0.0 2.300m [ops/s] 343k 440k 1.219m
clone 2.1.2 1.823m 96k 261k 263k
lodash.clonedeep 4.5.0 5.791m 219k 734k 1.988m
rfdc 1.1.4 32.823m 964k 2.420m 2.346m
Details
  • primitives: primitive objects; test case primitive
  • native object types: Array, Map, Set, and Boolean; test case obj types
  • plain objects: JSON-able object; test case Object :: plain small
  • arrays: small, dense, non-monkeypatched arrays of primitive values; test case Array :: pure hom dense_ small