A dependency-free utility for deeply cloning JavaScript objects.
import cloneDeep from 'cms-clone-deep';
const object = [{ foo: 'bar' }, { baz: { spam: 'eggs' } }];
const cloned = cloneDeep(object);
cloned.forEach(console.log);
// {foo: 'bar'}
// {baz: {spam: 'eggs'}}
console.log(cloned === object); // false
console.log(cloned[0] === object[0]); // false
console.log(cloned[1] === object[1]); // false
This module is compatible with TypeScript. See this section for more information.
First, install node.js on your machine. At the time of this writing, the current stable version is 20.10.0.
After that, using the terminal in any package, execute npm install cms-clone-deep
. In any ES6 module in that package, functions can be imported like so:
import cloneDeep, { cloneDeepFully, useCustomizers } from "cms-clone-deep";
If you would prefer an package which interops with commonjs modules, you can use the cms-clone-deep-es5
package which has the exact same api. Bundled and minified versions of both packages can be found at https://cdn.jsdelivr.net/npm/cms-clone-deep/+esm (cms-clone-deep
) and https://cdn.jsdelivr.net/npm/cms-clone-deep-es5 (cms-clone-deep-es5
). See the jsdeliver documentation for more information.
The first argument to cloneDeep
is the object to clone. The second optional argument is an object that can be used for configuration.
// Many ways to call `cloneDeep`:
import cloneDeep from 'cms-clone-deep';
let cloned;
let originalObject = {};
// 1: Default behavior
cloned = cloneDeep(originalObject);
// 2: Provide a configuration object
cloned = cloneDeep(originalObject, {
// Provide a function which extends the functionality of cloneDeep.
customizer: myCustomizer,
// An object can be provided which configures the performance of the
// algorithm.
performanceConfig: {
robustTypeChecking: true,
ignoreMetadata: false
},
// Warnings and Errors are typically logged to the console, but if a
// logging object can be provided, that will be used instead.
log: myLogger
});
To see the full list of options, please consult the API documentation.
structuredClone
has many limitations. It cannot clone objects with symbols. It does not clone non-enumerable properties. It does not preserve the extensible, sealed, or frozen status of the object or its nested objects. It does not clone the property descriptor associated with any values in the object.
cloneDeep
has none of these limitations. See this section for more about the differences between cloneDeep
and structuredClone
.
Functions cannot be reliably cloned in JavaScript.
eval
or the Function
constructor which are highly insecure.WeakMap
and WeakSet
instances also cannot be cloned.
Most objects have Object.prototype
or some other native JavaScript prototype in their prototype chain, but native functions cannot be cloned. This means that it is usually impossible to clone the prototype chain. Instead, it makes more sense to have the cloned object share the prototype of the original object.
Please see these notes for an in-depth discussion on the challenge of cloning functions.
When designing a deep clone algorithm, it is not possible to create a catch-all approach which clones all possible classes. One of the many reasons for this is that an algorithm cannot know which arguments should be passed to the constructor for the class, if any at all. Therefore, it is the responsibility of the class itself to determine how it can be cloned.
cms-clone-deep
provides a symbol CLONE
. If an object has a method associated with this symbol, then the return value of that method will be used as the clone. We will refer to this method as the "cloning method" for a class.
Suppose we had a class named Wrapper
which encapsulates a single private variable:
class Wrapper {
#value;
constructor(value) {
this.#value = value;
}
get() {
return this.#value;
}
set(value) {
this.#value = value;
}
}
Here is how we could add a cloning method to Wrapper
so that it can be cloned properly by cms-clone-deep
.
import cloneDeep, { CLONE } from "cms-clone-deep";
class Wrapper {
#value;
constructor(value) {
this.#value = value;
}
get() {
return this.#value;
}
set(value) {
this.#value = value;
}
[CLONE]() {
return {
clone: new Wrapper(this.get());
};
}
}
// create an object containing a wrapper instance and clone it
const wrapper = new Wrapper({ spam: "eggs" });
const obj = { foo: wrapper };
const clonedObj = cloneDeep(obj);
// check that it works
console.log(clonedObj === obj); // false
console.log(clonedObj.foo.get()); // {spam: 'eggs'}
console.log(clonedObj.foo.get() === obj.foo.get()); // false
For more details on cloning methods, please see the relevant documentation.
cloneDeep
can take a customizer which allows the user to support custom types. This gives the user considerable power to extend or change cloneDeep
's functionality.
For the sake of example, let us mend Wrapper
so that it can be cloned with a customizer instead of a custom method.
class Wrapper {
#value;
constructor(value) {
this.#value = value;
}
get() {
return this.#value;
}
set(value) {
this.#value = value;
}
}
Here is how we could create and use a customizer to properly clone Wrapper
instances.
import cloneDeep from "./clone-deep.js";
// The customizer gets one argument: the value to clone
function wrapperCustomizer(value) {
// If the customizer does not return an object, cloneDeep performs default behavior
if (!(value instanceof Wrapper)) {
return;
}
const clonedWrapper = new Wrapper();
return {
// the `clone` property stores the clone of `value`
clone: clonedWrapper,
// Let's clone the private property as well
additionalValues: [{
value: value.get(),
// `assigner` decides where the clone of `value.get` will be stored
assigner(cloned) {
clonedWrapper.set(cloned);
}
}]
};
}
// create an object containing a wrapper instance and clone it
const wrapper = new Wrapper({ spam: "eggs" });
const obj = { foo: wrapper };
const clonedObj = cloneDeep(obj, wrapperCustomizer);
// check that it works
console.log(clonedObj === obj); // false
console.log(Wrapper.isWrapper(clonedObj.foo)); // true
console.log(clonedObj.foo.get()); // {spam: 'eggs'}
console.log(clonedObj.foo.get() === obj.foo.get()); // false
If the customizer returns an object, the default behavior of cloneDeep
will be overridden, even if the object does not have a clone
property (in that case, the value will be cloned into the value undefined
).
There are many properties that will be observed in the object returned by the customizer. Please see the customizer documentation for more information.
cloneDeep
is the primary function that will be used from package, but cms-clone-deep
provides some additional functions:
1) cloneDeepFully
This function will clone an object as well as each object in its prototype chain.
2) cloneDeepAsync
This function will return the clone in a promise.
3) cloneDeepFullyAsync
Like cloneDeepAsync
, this function performs the behavior of cloneDeepFully
but wraps the result in a promise.
4) useCustomizers
This function takes an array of customizer functions and returns a new customizer. The new customizer calls each customizer one at a time, in order, and returns an object if once any of the customizers returns an object. Use this to avoid code reuse when creating multiple useful customizers.
Type information is provided for every function which can be imported from this package. In addition, the following types can be imported from cms-clone-deep
:
CloneDeepOptions
: The type of the object which can be passed as the second argument to cloneDeep
and cloneDeepAsync
.CloneDeepFullyOptions
: The type of the object which can be passed as the second argument to cloneDeepFully
and cloneDeepFullyAsync
.Customizer
: The type of the functions which can be used as customizers.Log
: The type of the optional logging object.CloneMethodResult
: Cloning methods can return this type of void
.CustomizerResult
: Customizer
s can return this type or void
.AdditionalValue
: The type of the objects which can be passed to the additionalValues
property in a CustomizerResult
.There are some features which are only accessible by cloning the repository. This is done by installing git. Once you have git
, execute git clone https://github.com/calebmsword/clone-deep.git
and a directory clone-deep/ will be made containing the source code. Then execute npm install
.
This repository uses type annotations in JSDoc to add type-checking to JavaScript. While this requires the typescript
module, there is no compilation step. The codebase is entirely JavaScript, but VSCode will still highlight errors like it would for TypeScript files. If you are using an IDE which cannot conveniently highlight TypeScript errors, then you can use the TypeScript compiler to check typing (npm i -g typescript
, then execute npm run tsc
).
The file clone-deep.test.js
contains all unit tests. Execute npm test
to run them. If you are using node v20.1.0 or higher, execute npm run test-coverage
to see coverage results.
We use eslint to lint this project. All merge requests to the dev and main branches must be made with code that throws no errors when linted. To run the linter, execute npm run lint
. To auto-format as much code as possible and then run the linter, execute npm run fix
. Note that the formatter is not guaranteed to force the code to pass the linter.
Some rudimentary benchmarking can be done within the repository. In the directory containing the source code, execute node serve.js <PORT>
, where the PORT
command line option is optional and defaults to 8787
, and visit http://localhost:<PORT>
to see the benchmarking UI. benchmark.js
and benchmark.css
contain the JavaScript and styling, respectively, for the hosted web page. You can use your favorite browser's dev tools to profile the result.
dev
branch with a descriptive name relevant to the issue title. Once you are finished with the implementation, create a pull request to the dev
branch.npm run verify-project
to check if the codebase will pass these three requirements. http.createServer
example.run-verify
npm script.