© 2019 Meouzer Consortium

JavaScript: Copying the Data Types and other Objects with valueCopy()

Meouzer The Values Based Copy Cat Making JavaScript Great Again! meouzer@gmail.com
loading
In JavaScript, value copying is the fundamental basis of all copying. Both shallow and deep copying rely heavily on value copying. So if you want to copy, it's imperative that you understand value copying. Meouzer

Introduction

Full code is at ValueCopy.js, which depends on typing.js. We will not cover everything in valueCopy.js.

The goal is to produce a ValueCopy() function that will serve as the basis for shallow and deep copying. The value copy of a source object is a target object whose internal state is the same but without any properties of its own: That's the way the concept was originally conceived, however JavaScript may force the value copy to have properties, but that's not a problem since after a copying source to target the properties are what they should be.

A value copy primitive is an element x for which ValueCopy(x) is supposed to be x. The value-copy-primitives are explicitly listed below.

The value-copy-primitives
  1. The primitives
  2. The host objects
  3. JavaScript's built in functions, and JavaScript's built in constructors.
  4. Class instances of Symbol, WeakSet, WeakMap.
    • It is impossible to read the internal state of Symbol, WeakSet and WeakMap class instances. It would be reasonable to throw exceptions on encountering Symbols, WeakSets and WeakMaps, but there's too much of a chance that copying them as is in deep copy operations may be just what you want.
  5. Prototypes of Built in classes.

It's also impossible to read the internal states of programmer defined class instances, but instead of being a value-copy-primitives, we will throw exceptions on encountering such because they are just too problematic.

The most arcane JavaScript elements treated in this article are null objects, which you can read about in Object Inheritance. The most arcane class is the fake Arguments class, which you can read about in JavaScript Data Typing isn't as Simple as you Think.

Of particular note, if x is an ArrayBuffer, a DataView, a Set or a Map, then a copy of x is simply x.valueCopy(). The same holds for all other built in classes except for Array and all the typed arrays. (Implicitly understood WeakMap and WeakSet excluded also).

If x is an array x.valueCopy(x) is the empty Array []. That's because indices are public properties of x and so the empty array has the same internal state as x but without any properties of its own. The next step in the copying process after valueCopy() will be to fill in the entries of the empty Array.

To show how ValueCopy can be used to shallow copy every type of built in class instance, and pretty much every object except for programmer defined class instances and value-copy-primitives, look at the following code.

The shallowCopy() Function
function shallowCopy(x) { const y = ValueCopy(x); if( y == x) return x; const keys = Object.getOwnPropertyNames(x); for(var i = 0; i < keys.length; i++) { const key = keys[i]; const pd = Object.getOwnPropertyDescriptor(x, key); Object.defineProperty(y, key, pd); } return y; } // Line 2: y has the same internal state as x. // Line 3: If x is a value-copy-primitive return x. // The rest of the code transfers the properties of x to y, // making sure property descriptors are copied. // Note: x and y share their properties, as the properties // themselves are not copied.

Of particular note, if x is an ArrayBuffer, a DataView, a Set a Map, an Array, a typed array or any other built in class then a copy of x is simply shallowCopy(x).

The Prototyped valueCopy() Methods

To eliminate the need for a huge switch statement in ValueCopy() to handle all the numerous built in classes, it's necessary to prototype ValueCopy(), in uncapitalized form, for the same numerous built in classes. Then ValueCopy() will employ those prototyped versions of valueCopy() and tie up some loose ends. To avoid naming conflict bugs in both node.js and the browsers, the name of the prototyped versions must be uncapitalized.

For example, to define valueCopy for Booleans we need to define Boolean.prototype.valueCopy. This obviously defines the valueCopy property of Boolean.Prototype itself. What should the value be? Well in a copy stream it doesn't hurt to copy built in class prototypes as is. So Boolean.prototype.valueCopy() is Boolean.prototype. Besides why would you want to valueCopy/shallow-copy/deep-copy Boolean.prototype even if you could?

If x is not a class instance of some class, and not a value-copy-primitive, then the value copy of x is y = Object.create(Object.getPrototypeOf(x)). Basically the internal state of x and y consist solely of their internal prototypes and those internal prototypes are equal.

Basic Classes

Basic ClassClass Instance xInternal Statex.valueCopy()Number Properties
Booleanx = new Boolean(true)true new Boolean(x.valueOf())0
Numberx = new Number(7)7 new Number(x.valueOf())0
Stringx = new String("cat")"cat" new String(x.valueOf)0
Datex = new Date()1617286697028new Date(x.valueOf())0
RegExpx = new RegExp(/abc/)/abc/ new RegExp(x.valueOf())1
Setx = new Set(["cat", "dog"])elements "cat", "dog" new Set(x.valueOf())0
Mapx = new Map([["a","b"],["c","d"]]) "a" mapped to "b" and "c" mapped to "d" new Map(x.valueOf())0

If x = new RegExp(/abc/) then y = new RegExp(x.valueOf()) has a single property lastIndex with a value possibly different than that for x. However, once the properties of x are transferred to y, the lastIndex properties of the two will be the same.

If source = new Boolean(true) then target = new Boolean(source.valueOf()) is a value copy of source. It has the same internal state true as the source and has no properties of its own. A similar discussion holds for all remaining classes of the table.

Boolean valueCopy as Template for the Basic classes
Object.defineProperty(Boolean.prototype, 'valueCopy', { value:function() { // primitives boxed if(isBoolean(this)) return new Boolean(this.valueOf()); if(this === Boolean.prototype) return this; return Object.create(Object.getPrototypeOf(this)); } }); // Line 3: if this is a class instance of Boolean // (i.e., a Boolean) then the value copy is returned as per the table. // Line 4: if this is an instance of Boolean but not a class instance // of Boolean the return has already been noted as the value copy.

Error Classes

If x is an instance of one of the error classes then x.valueCopy() will be an actual copy of x. This breaks the theme that the value copy should be empty, but makes the value copy more useful.

Error Classes valueCopy
function makeErrorClassValueCopiable(klass) { Object.defineProperty(klass.prototype, "valueCopy", { value:function() { if(isErrorVariant(this)) { const target = new klass(); if(hasOwnProperty(this, "fileName")) target.fileName= this.fileName; if(hasOwnProperty(this, "message")) target.message = this.message; if(hasOwnProperty(this, "number")) target.number = this.number; if(hasOwnProperty(this, "lineNumber")) target.lineNumber= this.lineNumber; if(hasOwnProperty(this, "columnNumber")) target.columnNumber = this.columnNumber; if(hasOwnProperty(this, "stack")) target.stack= this.stack; if(hasOwnProperty(this, "description")) target.description= this.description; return target; } if(this === klass.prototype) return this; return Object.create(Object.getPrototypeOf(this)); } }); } makeErrorClassValueCopiable(Error); makeErrorClassValueCopiable(InternalError); makeErrorClassValueCopiable(EvalError); makeErrorClassValueCopiable(ReferenceError); makeErrorClassValueCopiable(RangeError); makeErrorClassValueCopiable(SyntaxError); makeErrorClassValueCopiable(TypeError); makeErrorClassValueCopiable(URIError); // Line 4: Check to see if this is a class instance of some error class. // Line 5: Create another class instance of the error class // Line 6-19: Fill in all the possible properties, which vary by browser. // Line 20: return the value copy // Line 21: That's what we do with prototypes // Line 22. Thats what we do with instances of the // class that are not class instances of the class.

Typed Array Classes

All typed array class instances are fixed length arrays whose entries are accessible by index notation. If x is an instance of a typed array class then x.valueCopy() is an instance of the same class, and with the same fixed length as x. However, the entries of the value copy are all initialized to zero.

Int8Array value copy is Template for the other Typed Arrays
Object.defineProperty(Int8Array.prototype, 'valueCopy', { value:function() { if(isInt8Array(this)) return new Int8Array(this.length); if(this === Int8Array.prototype) return this; return Object.create(Object.getPrototypeOf(this)); } });

The Indeterminate Classes

The two indeterminate classes are WeakSet and WeakMap. There is no way to determine what the members of a WeakSet instance are. Thus it is impossible to duplicate a WeakSet instance. Since it's impossible to copy a WeakSet instance, a WeakSet instance is taken as a value-copy-primitive. A similar discussion holds for WeakMap.

WeakMap value copy is also a Template for WeakSet
Object.defineProperty(WeakMap.prototype, 'valueCopy', { value:function() { if(isWeakMap(this)) return this; if(this === WeakMap.prototype) return this; return Object.create(Object.getPrototypeOf(this)); } }); // Line 3: Class instances of WeakMap are returned as is. // Line 4: Thats what we do with instances of the // class that are not class instances of the class.

The Array Class

If x is an array then y = x.valueCopy() is an empty array [ ]. We don't want entries for the value copy because the next step in the copying process is to transfer the properties of x to y, which then fills in the empty array.

Array value copy
Object.defineProperty(Array.prototype, 'valueCopy', { value:function() { if(isArray(this)) return []; if(this === Array.prototype) return this; return Object.create(Object.getPrototypeOf(this)); } }); // Line 3: return the value copy of a class instance of Array // Line 5: Thats what we do with instances of the // class that are not class instances of the class.

The ArrayBuffer Class

  1. Let b1 be an ArrayBuffer.
  2. v1 = new Int8Array(b1) is a view of b1, which means v1.buffer = b1.
  3. v2 = new Int8Array(v1) is a copy of v1. v2 has an equivalent but different buffer than v1.
  4. Since b1 = v1.buffer is equivalent to v2.buffer. v2.buffer is a value copy of b1: It has the same internal state and doesn't introduce any new properties.
  5. However, v2.buffer expands to new Int8Array(v1).buffer, which in turn expands to new Int8Array(new Int8Array(b1)).buffer.
ArrayBuffer value copy
Object.defineProperty(ArrayBuffer.prototype, 'valueCopy', { value:function() { if(isArrayBuffer(this)) return new Int8Array(new Int8Array(this)).buffer; if(this === ArrayBuffer.prototype) return this; return Object.create(Object.getPrototypeOf(this)); } }); // Line 3-4: Return the value copy of a class instance of ArrayBuffer // Line 6: Thats what we do with instances of the // class that are not class instances of the class.

The DataView Class

Let dv1 be a DataView. dv1.buffer and dv1.buffer.valueCopy() are equivalent ArrayBuffers. Let dv2 = DataView(dv1.buffer.valueCopy()). Then dv2.buffer = dv1.buffer.valueCopy() is equivalent to dv1.buffer. Thus dv2 is a value copy of dv1. They both have the same internal state, and dv2 doesn't introduce any new properties.

DataView value copy
Object.defineProperty(DataView.prototype, 'valueCopy', { value:function() { if(isDataView(this)) return new DataView(this.buffer.valueCopy()); if(this === DataView.prototype) return this; return Object.create(Object.getPrototypeOf(this)); } }); // Line 3: Value copying a DataView amounts to value copy its buffer. // Line 4: That's what you do with prototypes. // Line 5: Thats what we do with instances of the // class that are not class instances of the class.

The Function Class

In general, value-copying/copying/deep-copying with functions involved is hopeless. That's because in general you don't know in advance details about a function's outer context. It is therefore imposssible to write an evaluator to copy the function into an appropriate context. Also multiple functions may enter the copy stream and it is highly unlikely a single evaluator will work for them all.

The prototyed version of valueCopy is Function.prototype.valueCopy(evaluator). When you write func.valueCopy(evaluator) just any evaluator won't do. You have to examine the details of the outer context of func and actually write a custom evaluator before you plug it into func.valueCopy().

However, it is actually possible that for a particluar programmer defined class, the class constructor can provide a single custom evaluator that works for all the instance methods and private functions and so deep copying a class instance will be possible if the class doesn't get too wild.

Summary

The valueCopy() function will copy class instances of the built in classes, no problem. That is except for the Array and typed array classes. The shallowCopy() function will copy class instances of all built in classes. shallowCopy() will also copy the property descriptors.

Of lesser interest, valueCopy() will copy all objects whose internal state consists solely of its __proto__ attribute. If x is such an object then Object.create(Object.getPrototypeOf(x)) is a value copy of x. Examples that cover the full gambit of such objects follow. Klass is either a built in class or a programmer defined class.

  1. x = Object.create(Klass.prototype)
    • Here Klass must be a built in class. If Klass is a programmer defined class, x can't be distinguished from a class instance of Klass, and valueCopy(x) throws an exception.
  2. x = Object.create(new Klass(...))
  3. x = Object.create(Object.create(Klass.prototype))
  4. x is an instance of Klass but not a class instance of Klass, where Klass is the most derived class for which x is an instance.
  5. x is not an instance of any class and not a value copy primitive.