calebmsword / clone-deep

A dependency-free utility for deeply cloning JavaScript objects.
MIT License
0 stars 0 forks source link

cms-clone-deep

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.

Links

Installation

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.

cloneDeep

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.

Why should I use cloneDeep? JavaScript has structuredClone now!

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.

what cannot be cloned

Functions cannot be reliably cloned in JavaScript.

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.

cloning custom classes

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.

customizers

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.

Additional package features

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.

cms-clone-deep and TypeScript

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:

cloning this repository

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.

TypeScript & JSDoc

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).

testing

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.

linting

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.

benchmarking

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.

contribution guidelines

acknowledgements