DEV Community

loading...

Deep Clone of JS Objects with circular dependency

salyadav profile image Saloni Yadav ・1 min read

Deep cloning JS objects has a plethora of blog posts and articles over the internet. But as I see, most or rather all of them end up with a solution of stringifying the JSON object and parsing them back. Somehow I am really unsettled with this solution. Is there any other way of deep clone a JS object?

Let's take some scenarios to discuss:
1- Not just one level of nested object, how about atleast 10?
2- What if after a certain point, there is circular dependency in the objects. (Ever heard of the tortoise-hare algorithm in linked list?) How will be approach cloning such an object?

P.S. I do not want to JSON.parse(JSON.stringify(obj)).

Edit:

Why I don't what to use JSON.stringify?
I came across this article on Medium.
And it was pretty convincing of certain loopholes in using JSON.stringify.

I agree that this is the most effective way to convert an object, but I am in a quest for a non-work-around solution. We will deal with performance of cloning later. For now, a base solution!

Discussion (10)

pic
Editor guide
Collapse
ndaidong profile image
Dong Nguyen

Let's keep it simple. Logic is quite clear: if the input is not object or array, return it. Otherwise, call the function recursively. To avoid circular, just create a history to save each of value it sees at every step. Here is my suggestion (not tested):

const isArray = (val) => {
  return Array.isArray(val);
};

const isObject = (val) => {
  return {}.toString.call(val) === '[object Object]' && !isArray(val);
};

const clone = (val, history = null) => {
  const stack = history || new Set();

  if (stack.has(val)) {
    return val;
  }

  stack.add(val);

  const copyObject = (o) => {
    const oo = Object.create({});
    for (const k in o) {
      oo[k] = clone(o[k], stack);
    }
    return oo;
  };

  const copyArray = (a) => {
    return [...a].map((e) => {
      if (isArray(e)) {
        return copyArray(e);
      } else if (isObject(e)) {
        return copyObject(e);
      }
      return clone(e, stack);
    });
  };

  if (isArray(val)) {
    return copyArray(val);
  }

  if (isObject(val)) {
    return copyObject(val);
  }

  return val;
};
Collapse
salyadav profile image
Saloni Yadav Author

Hi Dong, this is a charming solution. It worked beautifully for all the nested objects, arrays, and primitive.

However, I tested a couple of scenarios for the cyclic object. And I found a behavior which is quite interesting. I hope to figure this out together-

//original object
let original = {
    a: 1, 
    b: [1, 2],
    c: [1, [2, 3]],
    d: {
        d1: 1,
        d2: [1, 2],
        d3: {}
    }
};

original.d.d3 = original.d;

let deep = clone(original);
//after which I did the following modifications
deep.d.d1 = 'saloni';
deep.d.d3.d1 = 'yadav';

Alt Text
Alt Text

Observations:

  1. The first hierarchy of circular object is deep cloned
  2. From the second hierarchy onwards, the cloning is shallow, both the current object and the original object.

Required:

  1. The circular dependency should be shallow within the same object, so even when I change the first parent in the deep obj, all the corresponding children should point to the same obj.
  2. This should however, not effect the original object at all.

This is scenario is obviously a very complex version of our use case. And I think the solution lies in how we use the Set or the Map. However, I must mention that your solution works amazingly well for a non-circular n-depth object. So kudos and thank you for that!! 🤩

Collapse
ndaidong profile image
Dong Nguyen

my pleasure! Thank you for your compliments.
Shallowing is a big issue here because we need a deepClone method. Please try to fix it :)
In another reply you mentioned to lodash. I've taken a look on their solution:
github.com/lodash/lodash/blob/mast...
The problem seems to be more complicated than we think, so they need a lot of code to deal with.

Thread Thread
salyadav profile image
Saloni Yadav Author

Hey, thanks for sharing the source code. It is pretty huge with a lot of cases for various scenarios. Indeed looks like its way more twisted than a recursive call. Pretty interesting all the same!

Collapse
blindfish3 profile image
Ben Calder

IIRC the stringify/parse solution is often used for performance reasons. Recursively copying nested properties from objects can be slow...
As already suggested you can look at the lodash implementation, but that will have been written for performance and not legibility.

Collapse
salyadav profile image
Saloni Yadav Author

Hi Ben,
Thanks for bringing the performance issue in to the picture. This is obviously an important concern.
Although JSON.stringify solved 98% of the use-case. There is the 2% that I am targeting in this discussion:
1- Conservation of data not identified by JSONing. Like:

JSON.stringify({ key: Nan });
JSON.stringify({ key: Infinity });
// all will be converted to "{"key": null}"

2- Cyclic dependency.

Could you point me to Lodash's implementation if you have it hand? ( PS- I am not being lazy, i will google it anyways, but would like to be aware of whether i am at the right source :D ) Thankyou!!

Collapse
blindfish3 profile image
Ben Calder

So you could look at: github.com/lodash/lodash/blob/mast...

...and you'll discover why the usual suggestion is to not reinvent the wheel :D

TBH I'd only bother looking into a custom solution if lodash doesn't offer adequate performance and the stringify/parse approach isn't appropriate... That's not something I've had to worry about so far 🤷

Thread Thread
salyadav profile image
Saloni Yadav Author • Edited

Hi Ben,
I did take a look at the lodash implementation. And lord that's one hell of an source code! Also tried some suggestions by Dong, but looks like when things come to cyclic dependencies and a lot of corner cases, life isn't as simple!
And yes, it makes sense to only to bother about customizing the implementation when neither of the available solutions are performing as required. Thanks again! :D

P.S. It's going to take a while for me to make peace of Lodash's deep clone source code! 😂

Collapse
itsjzt profile image
Saurabh Sharma • Edited

Lodash does it (lodash.com/docs#cloneDeep), but Im not sure If it handles circular dependencies

Collapse
salyadav profile image
Saloni Yadav Author

Hey thank you! But then, my follow up question would be- How does lodash implement it? I am trying to get into the core of vanilla JS :) Thank you!