npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

cms-clone-deep

v2.0.0

Published

A dependency-free utility for deeply cloning JavaScript objects.

Downloads

8

Readme

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.

  • It is impossible to clone native JavaScript functions.
  • It is almost always impossible to clone functions which manipulate data in their closures.
  • The techniques which properly clone some functions use 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.

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:

  • 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: Customizers can return this type or void.
  • AdditionalValue: The type of the objects which can be passed to the additionalValues property in a CustomizerResult.

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

  • If you notice a bug or have a feature request, please raise an issue. Follow the default template provided for bug reports or feature requests, respectively.
  • If you would like to implement a bug fix or feature request from an issue, please create a branch from the 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.
  • All merge requests undergo a build pipeline that runs the full test suite, runs the linter, and runs the typescript compiler. Code cannot be merged into the dev or main branches without passing this pipeline. For convenience, run npm run verify-project to check if the codebase will pass these three requirements.

acknowledgements

  • This algorithm is a heavily modified version of the the cloneDeep utility from Lodash. Infinite thanks goes out to following GitHub users I could find who contributed to that algorithm:
  • Thanks to Tiago Bertolo, the author of this article. It reminded me to clone the metadata of objects.
  • Thanks to MDN and its documentation on the structured clone algorithm.
  • Thanks to StackExchange user Jonathan Tran for his http.createServer example.
  • Thanks to clone for its implementation of cloning Promises.
  • Thanks to this stackoverflow answer for its suggestion for cloning Files.
  • Thanks to fisker and their package for its implementation of creating FileList instances.
  • Thanks to this stackexchange post, which helped me implement the run-verify npm script.