deep-copy-system

deep copy anything including programmer defined classes.

Usage no npm install needed!

<script type="module">
  import deepCopySystem from 'https://cdn.skypack.dev/deep-copy-system';
</script>

README

Introduction to deep-copy-system

Deep copy anything and everything
including programmer defined classes

Specification, concepts, and code implementation were first published in 2019 at, https://meouzer.com and only recently at https://meouzer.github.io. These sites' articles clearly laid out the ideas, motivations, and a great amount of deep copying groundwork. If you want to get into the nuts and bolts of deep copying, these are must reads. Meouzer is pure silliness, but don't let him fool you! The code and concepts are dead serious. The two sites are now out of date with the most recent and best NPM code.

Specification of the deepCopy(x) and deepCopySimply(x) functions

  • Copies circular and duplicate references
  • Copies property descriptors
  • Copies frozen, sealed, and extensible states
  • Copies all data types
    • All Primitives
    • Boolean
    • Number
    • String
    • Date
    • RegExp
    • Array
    • All Typed Arrays
    • All Error Classes
    • Map
    • Set
    • DataView
    • ArrayBuffer
    • Buffer
    • Function (evaluator required)
    • getters/setters (evaluator required)
  • Copies secondary data objects in deep copy stream. E.g., y = Object.create(x), y = Object.create(Object.create(x)), etc. Here x can be any object, but if y is detected to be a class prototype, it is taken as a deep copy primitive (copied as is).
  • Copies Symbols, WeakMaps, and WeakSets as is since the programmer can't read their internal states.
  • Copies Promises as is
  • Copies complexity
    • E.g., Sets whose members are Maps whose keys are ArrayBuffers and values are typed arrays. The Set can also have properties that are DataViews whose properties are Maps whose properties are Sets.
    • Circular and duplicate references may have ends in the members of a Set, or the keys and values of a Map.

If you know about evaluators, then you may supply an evaluator as the second parameter as in deepCopy(x, evaluator) or deepCopysimply(x, evaluator). Here, x may be more than pure data by having sub-functions x.a.b.c..., which in turn may be getters/setters. The evaluator is used to copy these sub-functions into the appropriate context.

Secondary data objects, the edge case, and robustness

First some nomenclature :

  • If x instanceof Klass is true then x is an instance of Klass.
  • if x is created with the Klass constructor then x is a class instance of Klass.
    • Alternately if x = Object.create(Klass.prototype) and Klass.call(x,...) is called.
  • Secondary data objects of Klass are instances but not class instances of Klass.
  • The edge case is the secondary data object x = Object.create(Klass.prototype),
    where x is to be neither used as a class prototype, nor to become a class instance
    via Klass.call(x,...).

A deep copy algorithm is either robust or simple. Robust handles the edge case, while simple does not. deepCopy() is robust and deepCopySimply() is simple.

deepCopySimply(x) differs from deepCopy(x) in that it is more efficient at the cost of failure when x is the edge case x = Object.prototype(Klass.prototype) and x itself is not to be used as class prototype. deepCopySimply(x) should suffice almost all the time. When x is used as a class prototype, say of a class Klass, failure of deepCopySimply(x) does not occur because the programmer will write the code eval(makeCopiableSimply(Klass.name)) : this inures that x is copied correctly, class instances of Klass, are copied correctly, and secondary data objects of Klass are copied correctly.

Exports of deep-copy-system

export description
deepCopyExt() has options to handle or not circular/duplicate references, property descriptors, and sealed/frozen/extensible states. may choose robust or simple copying. may also choose which properties are to be copied.
deepCopy() deep copies everything.
wrapper around deepCopyExt()
deepCopySimply() deep copies everything except for edge case.
wrapper around deepCopyExt()
more efficient than deepCopy()
deepCopyES5Class function string used in robust deep copying of programmer defined ES5 classes
deepCopyES5ClassSimply function string used in simple deep copying of programmer defined ES5 classes
defaultConstructorSymbol symbol used in deep copying of programmer defined ES6 classes (both robust and simple)
copySymbol symbol used in robust deep copying of programmer defined ES6 classes
copySymbolSimply symbol used in simple deep copying of programmer defined ES6 classes
makeCopiable() used in robust deep copying of programmer defined ES6 classes
makeCopiableSimply() used in simple deep copying of programmer defined ES6 classes
deepCopySystem function string used to robustly deep copy systems of objects interrelated by their methods
deepCopySystemSimply function string used to simply deep copy systems of objects interrelated by their methods

OK! We got our first glance of how complicated it is to deep copy programmer defined ES6 classes.

Deep copying systems, though most likely of little interest, was developed for its own sake. However, the ideas behind deep copying systems lead to the development of deep copying programmer defined ES5 and ES6 classes. A class (instance) is basically a system that is nicely packaged.

Supports

Node.js, FireFox, Chrome, Opera, and Edge are supported. IE11 is not supported because symbols are used and IE11 does not support symbols.

Interestingly enough, robust typing, which robust deep copying relies on, is used to distinguish between the node.js and browser environments in a manner that can not be spoofed.

Usage

Deep copying data with deepCopy(x) and deepCopySimply(x)

    const {deepCopy} = require('deep-copy-system')
    
    x = complicated data object
    const y = deepCopy(x) // y is a deep copy of x
    
    --------- or ------------
    
    const {deepCopySimply} = require('deep-copy-system')
    
     x = complicated data object
    const y = deepCopySimply(x) // y is a deep copy of x

Deep copying data with deepCopyExt(x, params)

A peculiar feature is that you can custom filter the properties to be copied (see example below), but the author doesn't see much need for that.

    const {deepCopyExt} = require('deep-copy-system')    
    x = complicated data object
    
    /*  As an example, specify that 
        (1) circular/duplicate references are to be copied,
        (2) property descriptors are not to be copied
        (3) frozen/sealed/extensible states are to be copied
        (4) Only enumerable properties are to be copied 
        (5) algorithm is simple (edge case sacraficed for efficiency)
        */
    
    const y = deepCopy(x, {cd:true, pd:false, freeze:true, getProperties:Object.keys, simple:true} )
    
    // note that Object.keys(x) are the enumerable properties of x.
    
    /* (a)  params, if unspecified, defaults to {}
       (b)  The optional parameters cd, pd, and freeze all default to false.
       (c)  getProperties defaults to Object.getOwnPropertyNames.
            You may create your own custom getProperties function.
       (d)  simple defaults to false, where robust algorithm used.     
       (e)  There may be a params.evaluator property. This is mainly for 
            internal use by the library. But you can use it if you know
            about evaluators (See meouzer.com or meouzer.github.io for details). */

Robust deep copying of programmer defined ES5 classes

Unlike ES6 classes where there are a number of hoops to jump through, ES5 deep copy class code is short and sweet as a one liner.

const {deepCopy, deepCopyES5Class, writeVariables, cleanContext} = require('deep-copy-system')

function Foo(x,y) // Foo is an ES5 class
{
    // variables. functions that aren't methods are variables.
    var a = ...;  // or function a(){...}
    const b = ...; // b could be function
    let c = ...;   // c could be function
    
    /* a, b, c, and their sub-objects may have their own methods 
       that depend solely on Foo's scope and outer context.
       Remember, even functions may have their own methods.*/
    
    // Write methods here. 
    // Methods that are getters/setters allowed.

    (function nestedFunction(w,x)
    {
        var/const/let y;
        
        /* No class methods, private functions of Foo,
           or methods of class data are to be defined 
           inside any nested function, unless they don't 
           depend on the local w,x,y. But then why define
           n a nested function in the first place? */
         
        // Changes to pure data variables of the class allowed.
    })(5,7);

    this.deepCopy = function()
    {
        // deepCopy() is short an sweet
        return eval(deepCopyClass)({this$:this, a:a, "const b":b,
            "let c":c, x:x, y:y}).this$;
            
        // this$ must come first, but you may use a different name.  

        /* This is good! Don't make the mistake of using a local 
           variable, because it would end up in the outer context 
           of the sub functions of the deep copy. Not necessarily
           adverse, but the author doesn't like it. */   
    }
}

const x = new Foo(1,2);
const y = x.deepCopy(); // y is a deep copy of x 
const z = y.deepCopy(); // z is a deep copy of y

// That's all there is to it. 

Simple deep copying of programmer defined ES5 classes

Same as previous section, but in imports and code, replace the use of deepCopy with deepCopySimply and deepCopyES5Class with deepCopyES5ClassSimply

Robust deep copying of programmer defined ES6 classes

Deep copying ES6 classes is fairly involved. There are five steps to follow as indicated in the following outline. Step 2 has three sub-steps. Step 4 has six sub-steps.

// (1) First, make sure you have the needed imports

const {deepCopy, defaultConstructorSymbol, copySymbol, makeCopiable} = require('deep-copy-system')
const {type, makeTypable} = require('type-robustly')

class Foo // Foo is an ES6 class 
{        
    #F = ...; // private field (data or method)
   
    // (2) Second, write a specialized constructor
    constructor(x, y)
    {
        // (2a) make the ES6 class typable
        // E.g., if x = new Foo(...), then type(x) is "Foo"
        eval(makeTypable('Foo'));
        
        // (2b) return empty class instance of Foo 
        if(arguments[0] === defaultConstructorSymbol) return;
        
        // (2c) Defer to #initialize for normal construction
        this.#initialize(x,y); 	
        
        /* The deep copy process is expecting an "empty" 
           class instance. If you write normal constructor code 
           here, vars and functions are hoisted, which means
           an "empty" class instance may not really be empty:
           Who knows what JavaScript is doing behind the scenes?
           This is perfectly OK as long as you don't mind
           the possiblility of a memory leak, as the deep
           copy process will override these vars, and functions.
           That is, can you be sure these overriden vars, and 
           functions are garbage collected? Besides, you 
           shouldn't involve garbage collection in the first 
           place if that indeed happens! */
    }
    
    // (3) Third, write initializer field.    
    #initialize = function(x,y)
    {
        // normal constructor code goes below
        
        // variables (ES5 ideas apply - see ES5 example above)
        var a = ...;
        const b = ...;
        let c = ...;
        
        this.#G = ...; // private field (variable ideas apply - see above)
        
        // (4) Fourth, write the deepCopy() method inside #initialize()    
        this.deepCopy = function()
        {
            // (a) all variables and private fields report to context object
            const cleanContextObject = {this$:this, a:a, b:b, c:c, x:x, y:y,
                "#F":this.#F, "#G":this.#G, "#initialize":this.#initialize};
                
            // a name other than "thisquot; made be used to refer to this.
            // make sure the same name is used in step f.


            // If you can figure out the following function, then you are a 
            // bona-fide expert at context and enclosures.
            
            return(function ()
            {
                // (b) define evaluator for copying methods and functions
                this.evaluator = function(){return eval('('+ arguments[0] +')');};
                
                // (c) deep copy the context object
                this.contextObject = deepCopy(cleanContextObject, this.evaluator);
                
                // (d) provide the context for the evaluator.  Make sure you 
                // mimic the vars/lets/consts of the class. Constructor
                // parameters should use var.
                
                var a = this.contextObject.a;
                const b = this.contextObject.b;
                let c = this.contextObject.c;
                var x = this.contextObject.x;
                var y = this.contextObject.y;
                this.contextObject.this$.#F = this.contextObject["#F"];
                this.contextObject.this$.#G = this.contextObject["#G"];
                this.contextObject.this$.#initialize = this.contextObject["#initialize"];

                // (e) return the deep copy of the class system
                return this.contextObject;
                }).call({}).this$ // (f) extract the deep copy of the class instance of Foo.
            } 
        }	
    }
}

 // (5) Fifth, make the ES6 class copiable
 eval(makeCopiable('Foo')); 
 
const x = new Foo(1,2);
const y = x.deepCopy(); // y is a deep copy of x
const z = y.deepCopy(); // z is a deep copy of y

Simple deep copying of programmer defined ES6 classes

Same as previous section, but in imports and code, use of deepCopy() is to be replaced with deepCopySimply(), and use of makeCopiable() is to be replaced with makeCopiableSimply()

Deep Copying Systems (other than classes)

The programmer won't do much of this if at all, but if interested you can do your own research at meouzer.com or meouzer.github.io. Basically to deep copy an object you may have to deep copy many objects simultaneously because they all belong to the same system interrelated by their methods.

Testing

In the node-modules/deep-copy-system folder, there is a Test folder with eight test files. Each test file uses weirdly complicated constructs to test deep copying to the max.

The larger picture and library

  • The type-robustly package (is used by all following packages)
    • The type(x) function is its main export.
  • The deep-copy-system package
    • aphelion = deep copy programmer defined ES5 and ES6 classes.
  • The serialize-nicely package
    • aphelion = serialize/deserialize programmer defined ES5 and ES6 classes.
  • deep-equal-diagnostics
    • Gives reason why two objects are not deep copies of each other or states that they are deep equals.
  • The deep-copy-quickly package is written for efficiency when the following
    don't matter
    • copying circular and duplicate references (very expensive)
    • copying property descriptors
    • copying frozen/sealed/extensible states

The most important idea is that robust typing leads to robust deep copying (and serialization/deserialization). Deep copies of instances of data classes should start in a totally blank/empty state and implementation of such should be prototyped and make use of typing. Typing eliminates the "false positives" of instanceof. For example, if x is an Array then y = Object.create(x) is reported by instanceof to be an instance of Array. However type(y) correctly returns "Object", indicating that y is not an Array and is to be copied in its own particular way as a secondary data class object.

A challenge is that one really needs to sit down and figure out how to copy getter/setters, functions, and methods. Figuring out that eval needs to be used is just the obvious end of the stick as providing the necessary context for the copied getters/setters, functions, and methods is the tricky part: just look at the ES6 class example above.

deepCopy() was originally implemented in non-recursive form using a stack. A stack is just an Array with either its push/pop methods or with incrementing/decrementing indices. However, in JavaScript recursive forms are faster. This is just the opposite of C++. Apparently in JavaScript, a stack is significantly less efficient than the call-stack. Now in NPM, deepCopy() is implemented recursively.

There also is (will be) the type-simply package. Simple deep copying relies on simple typing, just as robust deep copying relies on robust typing. However, that's in theory because in simple deep copying, it's best just to inline simple typing. So, the type-simply package actually isn't used in the deep-system-copy package.

Version history

1.0.0 published 10-30-2021

1.0.1 published 11-5-2021

  • Fixed bug in deepCopy.js that prevented typed arrays from being copied correctly.
  • Fixed bug in Tests/stringify.js