A bit of functional fun

I love it when your code unravels, I guess that is the importance of proper testing. I have amended deepclone to handle dates and any primitive values that maybe passed in.

Here is an update. If any howlers stand out I would appreciate the feedback.

Deep Cloning in JS

Using Property Descriptors

We are going to look at a deep clone solution utilising two built-in Object methods getOwnPropertyDescriptors and defineProperties.

Shallow Copying

To start with let’s first have a look at a typical example of shallow copying and the potential pitfalls we may encounter with this approach.

const person = {
   name: ['Fred', 'Flinstone'],
   age: 45
}
// shallow copy using ...spread operator
const clonedPerson = {...person}
clonedPerson.name[0] = 'Barney'
clonedPerson.name[1] = 'Rubble'

console.log(clonedPerson)
// { name: ['Barney', 'Rubble'], age: 45 } all good

console.log(person)
// { name: ['Barney', 'Rubble'], age: 45 } Not good!

We can see by making changes to our cloned object we have also modified our source object.

The reason for this is that we are dealing with a reference type in the form of a name array property. The property doesn’t hold data or values, but rather a pointer or an address to a particular place in memory where the array data is held.

When we make a shallow copy, the name property on the cloned person object also points to that same address in memory. Therefore any changes we make to that data will be reflected in both objects.

Property Descriptors

Carrying on for a moment with shallow copying we are going to look at an alternative approach to the spread operator focusing on property descriptors.

Let’s enhance our original object by adding a getter accessor method which will return the full name of person.

const person = {
   name: ['Fred', 'Flinstone'],
   age: 45,
   // getter function
   get fullName() {
     return this.name.join(' ')
   }
}

console.log(person.fullName)
// Fred Flinstone

Again we will make a clone of the person object and modify it’s properties

const clonedPerson = {...person}
clonedPerson.name[0] = 'Barney'
clonedPerson.name[1] = 'Rubble'

console.log(person.fullName)
// Barney Rubble - Not what we wanted, but to be expected

console.log(clonedPerson.fullName)
// Fred Flinstone - What's going on here?

An odd result. Albeit we modified our cloned object, fullName is returning the original source object’s name. If we inspect the property descriptor focusing in on the getter function our issue becomes a little bit clearer.

Person

// source object
console.dir(Object.getOwnPropertyDescriptor(person, 'fullName'))

Object
    configurable: true
    enumerable: true
    get: ƒ fullName() // getter function
    set: undefined

Cloned Person

// cloned object
console.dir(Object.getOwnPropertyDescriptor(clonedPerson, 'fullName'))

Object
    configurable: true
    enumerable: true
    value: "Fred Flinstone" // replaced with value
    writable: true

On copying the source object the getter function has been evaluated and replaced with a value. You may also notice that albeit we didn’t create a setter function it has been replaced as well.

To solve this issue we can use getOwnPropertyDescriptors and defineProperties instead to make a shallow copy.

const clonedPerson = Object.defineProperties(
    {}, // target object to clone to
    Object.getOwnPropertyDescriptors(person)
)

console.dir(Object.getOwnPropertyDescriptor(clonedPerson, 'fullName'))

Object
    configurable: true
    enumerable: true
    get: ƒ fullName() // getter function intact
    set: undefined

Clone Target Object

Before moving on we need to tackle one issue and that is the target object we are cloning to. Again the best way to illustrate this is with an example.

const person = ['Fred']

const clonedPerson = Object.defineProperties(
    {}, // target object to clone to
    Object.getOwnPropertyDescriptors(person)
)

console.log(clonedPerson)
// {0: 'Fred', length: 1}

clonedObject.push('Rubble') // TypeError: clonedObject.push is not a function

You can see by looking at the curly parentheses on the logged output that the original Array has been converted to an Object, or what is more commonly called an Array-like object. In the process we have lost access to the Array.prototype and it’s methods e.g. push. We therefore need a way of checking the source object type to make sure our target matches that type.

Conveniently the solution to this comes in the form of Object.prototype.constructor, which returns an empty object matching the object the constructor is called on.

const obj = {a: 1, b: 2}
const arr = [1, 2, 3]

arr.constructor() // -> [] empty array
obj.constructor() // -> {} empty object

The clone can now be amended accordingly. Let’s wrap this up with a shallow clone function.

const shallowClone = (sourceObject) =>
    Object.defineProperties(
        sourceObject.constructor(),
        Object.getOwnPropertyDescriptors(sourceObject)
    )

Working with Nested Properties

On to the meat and potatoes of our deep clone function — dealing with nested properties. These nested properties will be held inside our two reference types Arrays and Objects.

On running through the object properties if we come across either of these types we will know we need to dig a bit deeper into our object, and to do this we will use recursion. By using a combination of recursion and shallow copying we will end up with two objects with distinct object properties.

Final Script

The best way I think to explain the process is to have a look at the final script and do a bit of a breakdown. We will start with writing a couple of helper functions we can utilise in our final script to determine if the current property is an Array or an Object.

// Helper functions
const isArray = obj => Array.isArray(obj)
const isObject = obj => ({}.toString.call(obj) === '[object Object]')
// final deepClone script
const deepClone = function deepClone (source) {
    if (source instanceof Date) return new Date(source.getTime())

    if (isArray(source) || isObject(source)) {
        const clone = shallowClone(source)

        for (const [key, prop] of Object.entries(clone)) {
            if (typeof prop === 'object') clone[key] = deepClone(prop)
        }
        return clone
    }
    return source
}

The first line of deepClone’s execution checks to see if the source object is a Date and if so returns a copy.

The second line checks to see whether we have a collection of properties in the form of an Object or an Array and if so we make a shallow clone of that object.

Using Object.entries an array of [key, value] pairs from the shallow clone is created which we then loop through. If the current property in the loop is an object type the property is passed to a recursive call of deepClone deepClone(prop) and whatever is eventually returned replaces the shallow clone property with that key clone[key] as a new value.

The recursive process is repeated, digging deeper and deeper into the object until it reaches the innermost properties. In other words we have reached a point where the passed source object doesn’t contain any properties matching a type of object.

It is this that serves as what is known as a base condition and is the point at which clones are finally returned one after another or more accurately with the clone key assignment one inside another. Here is an example.

// object to clone
{ a: { b: { c: 2 }, d: 3 } e: 4 }

// From the tailend
shallow clone of { c: 2 } is returned overwriting 'b'
shallow clone of { b: { c: 2 }, d: 3 } is returned overwriting 'a'
shallow clone of { a: { b: { c: 2 }, d: 3 } e: 4 } is returned out of the function

The final line in the code return source returns any primitive values that may have been passed to deepClone.

On to testing.

Source test object

Carrying on the theme the following person object has a personalInfo property which contains a name and address. In addition it has a getter and setter that will read and update the nested address property.

const person = {
    date: new Date(),

    personalInfo: {
        name: ['Fred', 'Flinstone'],
        address: {
            road: '222 Rocky Way',
            city: 'Bedrock 70777'
        }
    },

    get address () {
        return this.personalInfo.address
    },

    set address (newAddress) {
        const { personalInfo } = this
        personalInfo.address = {...personalInfo.address, ...newAddress}
    }
}

const clonedPerson = deepClone(person)

Running some basic tests

We will test to see if the cloned object has functional getters and setters and that we are able to modify nested properties of the clone without mutating the sources object.

To start with a test to confim that the accessors are indeed functions.

// using a simple helper function for readibility
const address = getOwnPropDescriptor(clonedPerson, 'address')

console.log(typeof address.get === 'function') // true
console.log(typeof address.set === 'function') // true

Then a simple modification to the cloned object’s address property and some test outputs.

clonedPerson.address = {road: '301 Cobblestone Way'}

console.log(clonedPerson.address)
// {road: '301 Cobblestone Way', city: 'Bedrock 70777'}

console.log(person.address)
// {road: '222 Rocky Way', city: 'Bedrock 70777'}

We can see that we are able to successfully modify the nested address property on our clone without affecting the same property on our source object.

Conclusion

The examples I have given here of copying objects and their accessors do fall very much into ‘edge case scenario’. That said given the relative simplicity of using property descriptors with it’s ability to copy all own properties of an object I do think it is a worthy exploration.