DEV Community

disgusting-dev
disgusting-dev

Posted on

Classic issues: Deep Copy

The Issue

I need to get a full copy of some object. Let's say I have a plain object full of primitives:

const objExample = {
  n: 63,
  s: 'An',
  b: false,
  u: undefined,
  e: null,
};
Enter fullscreen mode Exit fullscreen mode

Before I start

I always like to repeat, that some sort of classic issues analysis is nothing more thant just analysis - cause if you guys need deep copy, better go to 'lodash.cloneDeep' (not even talking about cloneDeepWith), they do write code for coders and that simply means a lot.


Shallow Copy

I can use Object.assign or spread operator to try to clone that:

const assignedObject = Object.assign({}, objExample);
const spreadedObject = { ...objExample };
Enter fullscreen mode Exit fullscreen mode

Of course, this is just 2 different syntax of the same operation, so no surprise that the result is going to be the same - our objExample will be copied to 2 different variables

But, this copying called 'Shallow Copy' - which means that it's ok working with primitives, but for structural types it will copy not a body but the reference to the structure being copied

const objExample = {
  b: false,
  c: { a: 34 }
};
const assignedObject = Object.assign({}, objExample);

assignedObject.b = 24;
assignedObject.c.a = 45;

console.log(objExample.b, objExample.c); // false { a: 45 }
Enter fullscreen mode Exit fullscreen mode

How we can avoid this?

We can try with JSON serialization/deserialization technique:

const jsonObject = JSON.parse(JSON.stringify(objExample));

jsonObject.c.a = 63;

console.log(objExample.c);
Enter fullscreen mode Exit fullscreen mode

But

JSON may work only with JSON-like structures, which means that you are not able to work with non-json stuff like functions, undefined etc.

const objExample = {
  u: undefined,
  e: () => {},
  b: new Date(),
  m: new Map(),
  c: { a: 34 }
};

const jsonObject = JSON.parse(JSON.stringify(objExample));
console.log(jsonObject);
//Output: { b: '2021-03-15T08:06:01.181Z', m: {}, c: { a: 34 } }
Enter fullscreen mode Exit fullscreen mode

So JSON doesn't work well here.
On another hand, Node.js has own package 'v8' that also has serialization methods with Buffer under the hood, but it doesn't support functions copying, so this is also not for us.

const v8 = require('v8');

const objExample = {
  u: undefined,
  a: function() {},
  m: new Map(),
};

const v8Object = v8.deserialize(v8.serialize(objExample));
//Error: function() {} could not be cloned;
Enter fullscreen mode Exit fullscreen mode

No choice - I need to write own solution
(If I don't know about lodash of course)


Define a function

Let's start with first step - we need to define a function and say there, that for starter we will work only with arrays and objects as complex structures:

function isObject(value) {
  return typeof value === 'object';
}

function deepCopy(value) {
  if (Array.isArray(value)) {

  } else if (value && isObject(value) {

  }
}
Enter fullscreen mode Exit fullscreen mode

I need to add a variable to operate with in all cases and to return at the end. Also, I need to say that if my function param is a primitive or simple function - I will just rewrite this variable:

function deepCopy(value) {
  let newVal;

  if (Array.isArray(value)) {

  } else if (value && isObject(value) {

  } else {
    newVal = value;
  }

  return newVal;
}
Enter fullscreen mode Exit fullscreen mode

If I work with object type, I need to go through all it's keys and specify - if the key is primitive, I'm adding it to my 'newVal', otherwise I'm recursively calling my function to go through internals of nested object:

else if (value && isObject(value)) {
    newVal = {};

    Object.keys(value).forEach(key => {
      if (isObject(value[key])) {
        newVal[key] = deepCopy(value[key]);
      } else {
        newVal[key] = value[key];
      }
    });
  }
Enter fullscreen mode Exit fullscreen mode

And for the array structure I just need to use map method with calling deepCopy there:

if (Array.isArray(value)) {
    newVal = value.map(item => {
      return deepCopy(item);
    });
  }
Enter fullscreen mode Exit fullscreen mode

Circular References

We need to develop the logic for preventing memory leak cases, when object has a field referenced with the object itself, which will create an infinite recursion and stack overflow

const a = { b: { c: 345 } };
a.d = a;
const b = deepCopy(a);
Enter fullscreen mode Exit fullscreen mode

For this, I need to use Map structure to set already existed keys there (WeakMap doesn't suit cause I also want to store arrays as keys)

function deepCopy(value, hash = new Map()) {
  let newVal;

  if (hash.has(value)) {
    return hash.get(value);
  }

  if (Array.isArray(value)) {
    hash.set(value, newVal);

    newVal = value.map(item => {
      return deepCopy(item, hash);
    });
  } else if (value && isObject(value)) {
    newVal = {};

    Object.keys(value).forEach(key => {
      if (isObject(value[key])) {
        hash.set(value, newVal);
        newVal[key] = deepCopy(value[key], hash);
      } else {
        newVal[key] = value[key];
      }
    });
  } else {
    newVal = value;
  }

  return newVal;
}


const a = { b: { c: 345 } };
a.d = a;
const b = deepCopy(a);

console.log(b); //{ b: { c: 345 }, d: [Circular] }
Enter fullscreen mode Exit fullscreen mode

P.S.

That's of course not the perfect end for such function, cause there are a lot of corner cases to check, but if we just go to starting code of lodash's cloning function baseClone...

/** `Object#toString` result references. */
const argsTag = '[object Arguments]'
const arrayTag = '[object Array]'
const boolTag = '[object Boolean]'
const dateTag = '[object Date]'
const errorTag = '[object Error]'
const mapTag = '[object Map]'
const numberTag = '[object Number]'
const objectTag = '[object Object]'
const regexpTag = '[object RegExp]'
const setTag = '[object Set]'
const stringTag = '[object String]'
const symbolTag = '[object Symbol]'
const weakMapTag = '[object WeakMap]'

const arrayBufferTag = '[object ArrayBuffer]'
const dataViewTag = '[object DataView]'
const float32Tag = '[object Float32Array]'
const float64Tag = '[object Float64Array]'
const int8Tag = '[object Int8Array]'
const int16Tag = '[object Int16Array]'
const int32Tag = '[object Int32Array]'
const uint8Tag = '[object Uint8Array]'
const uint8ClampedTag = '[object Uint8ClampedArray]'
const uint16Tag = '[object Uint16Array]'
const uint32Tag = '[object Uint32Array]'
Enter fullscreen mode Exit fullscreen mode

So I suggest to trust that function, or if something really bothers you - it's open source, so just fork it and enjoy any changes you would like to implement there;

Thank you for attention, hope you will like the format!

Top comments (2)

Collapse
 
patarapolw profile image
Pacharapol Withayasakpunt

JSON.stringify not working for you? Just use YAML.

Also, JSON functions' second arg can used to notify of object types.

I wrote a custom serialization some time ago.

GitHub logo patarapolw / any-serialize

Serialize any JavaScript objects, as long as you provides how-to. I have already provided Date, RegExp and Function.

I normally wouldn't just trust lodash for deepclone or deepmerge. So, I would write a minimal version for my own.

Collapse
 
disgustingdev profile image
disgusting-dev

Thank you for comment mate, and the serialization looks very mature

JSON usually works fine, it was mentioned there as a highlighter that "this is helpful not in 100% of cases", for 100% let's say that everyone will need to write/reuse what you wrote, which shows some necessity of corner cases development rather than JSON.stringify(JSON.parse)