3

Sometime ago, I read about the proposal of the new method Object.fromEntries() that is supported on newer versions of some browsers (reference). While reading about it, I had in mind the idea of use this method to deep-clone an object in replacement of using JSON.parse(JSON.stringify(obj)). So far, I have come with the next recursive approach that from my point of view appears to work.

const obj = {
  key1: {key11: "key11", key12: "key12", key13: {key131: 22}},
  key2: {key21: "key21", key22: "key22"},
  key3: "key3",
  key4: [1,2,3,4]
}

const cloneObj = (obj) =>
{
    if (typeof obj !== "object")
       return obj;
    else if (Array.isArray(obj))
       return obj.slice();

    return Object.fromEntries(Object.entries(obj).map(
        ([k,v]) => ([k, cloneObj(v)])
    ));
}

// Clone the original object.
let newObj = cloneObj(obj);

// Make changes on the original object.
obj.key1.key11 = "TEST";
obj.key3 = "TEST";
obj.key1.key13.key131 = "TEST";
obj.key4[1] = "TEST";

// Display both objects on the console.
console.log("Original object: ", obj);
console.log("Cloned object: ", newObj);
.as-console {background-color:black !important; color:lime;}
.as-console-wrapper {max-height:100% !important; top:0;}

However, I'm in doubt if I'm not missing something to accomplish the deep-clone of an object. So my questions are:

1) I'm missing something important in order to accomplish the deep-clone of an object?

2) In the case the approach is all right, do you believe this can performs better than using JSON.parse() and JSON.stringify()?

Thanks in advance!


Update 1

Here is the updated version with the feedback provided on the answers:

const obj = {
  key1: {key11: "key11", key12: "key12", key13: {key131: 22}},
  key2: {key21: "key21", key22: "key22"},
  key3: "key3",
  key4: [1,2,3,{key: "value"}]
}

const cloneObj = (obj) =>
{
    if (Object(obj) !== obj)
       return obj;
    else if (Array.isArray(obj))
       return obj.map(cloneObj);

    return Object.fromEntries(Object.entries(obj).map(
        ([k,v]) => ([k, cloneObj(v)])
    ));
}

// Clone the original object.
let newObj = cloneObj(obj);

// Make changes on the original object.
obj.key1.key11 = "TEST";
obj.key3 = "TEST";
obj.key1.key13.key131 = "TEST";
obj.key4[1] = "TEST";
obj.key4[3].key = "TEST";

// Display both objects on the console.
console.log("Original object: ", obj);
console.log("Cloned object: ", newObj);
.as-console {background-color:black !important; color:lime;}
.as-console-wrapper {max-height:100% !important; top:0;}

Update 2

Added a performance comparison in relation to my second question:

Tested on Firefox 66.0.3 (64bits):
Test_JSON: 1923.000ms
Test_cloneObj: 2047.000ms

Tested on Chrome 73.0.3683.103 (64 bits):
Test_JSON: 2276.560ms
Test_cloneObj: 1903.675ms

const cloneObj = (obj) =>
{
    if (Object(obj) !== obj)
       return obj;
    else if (Array.isArray(obj))
       return obj.map(cloneObj);

    return Object.fromEntries(Object.entries(obj).map(
        ([k,v]) => ([k, cloneObj(v)])
    ));
}

// Generate an object.

const getRandom = (min, max) => Math.floor(Math.random() * (max - min) + min);

let obj = {};

for (let i = 0; i < 100000; i++)
{
    obj["Array" + i] = Array.from({length: 100}, () => getRandom(0, 1000));
    obj["Obj" + i] = {"key": getRandom(0, 1000)};
    obj["Const" + i] = "some_string";
}

// Test performance on JSON.parse()/stringify()

console.time("Test_JSON");
let obj1 = JSON.parse(JSON.stringify(obj));
console.timeEnd("Test_JSON");

// Test performance on cloneObj().

console.time("Test_cloneObj");
let obj2 = cloneObj(obj);
console.timeEnd("Test_cloneObj");
.as-console {background-color:black !important; color:lime;}
.as-console-wrapper {max-height:100% !important; top:0;}
Community
  • 1
  • 1
Shidersz
  • 16,846
  • 2
  • 23
  • 48

3 Answers3

2

You are missing one thing:

else if (Array.isArray(obj))
  return obj.slice();

This will return a shallow copy of the array. If the array contains objects, those underlying objects will not be cloned:

const obj = [
  ['foo']
];

const cloneObj = (obj) =>
{
    if (typeof obj !== "object")
       return obj;
    else if (Array.isArray(obj))
       return obj.slice();

    return Object.fromEntries(Object.entries(obj).map(
        ([k,v]) => ([k, cloneObj(v)])
    ));
}

// Clone the original object.
let newObj = cloneObj(obj);

// Make changes on the original object.
obj[0][0] = 'bar';

// Display both objects on the console.
console.log("Original object: ", obj);
console.log("Cloned object: ", newObj);
.as-console {background-color:black !important; color:lime;}
.as-console-wrapper {max-height:100% !important; top:0;}

To fix it, return obj.map(cloneObj); instead:

const obj = [
  ['foo']
];

const cloneObj = (obj) =>
{
    if (typeof obj !== "object")
       return obj;
    else if (Array.isArray(obj))
       return obj.map(cloneObj);

    return Object.fromEntries(Object.entries(obj).map(
        ([k,v]) => ([k, cloneObj(v)])
    ));
}

// Clone the original object.
let newObj = cloneObj(obj);

// Make changes on the original object.
obj[0][0] = 'bar';

// Display both objects on the console.
console.log("Original object: ", obj);
console.log("Cloned object: ", newObj);
.as-console {background-color:black !important; color:lime;}
.as-console-wrapper {max-height:100% !important; top:0;}
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
1
  1. The deep-cloning is fine, as deeply nested objects still retain their properties - however, arrays need to be cloned too; replaced obj.slice() with obj.map(o => cloneObj(o)).
  2. This method is actually faster than JSON.parse(JSON.stringify(obj)) - three tests on JSBench and JSON was slower by more than 10% every single time.
Jack Bashford
  • 43,180
  • 11
  • 50
  • 79
  • What browser did you run the check on? `cloneObj` runs notably faster for me as well on Chrome 73, but I don't have FF 63 installed unfortunately. It may be implementation-dependent. – CertainPerformance Apr 16 '19 at 05:10
  • 1
    I ran it on the latest Chrome @CertainPerformance (`Object.fromEntries` doesn't work on Safari). – Jack Bashford Apr 16 '19 at 05:16
  • Thanks for your help too. I really don't have too much faith on the performance, since the existence of [this](https://stackoverflow.com/questions/122102/what-is-the-most-efficient-way-to-deep-clone-an-object-in-javascript/5344074#5344074) thread says `JSON.parse()` and `JSON.stringify()` is better. However, it is good to know it can work. – Shidersz Apr 16 '19 at 05:22
1

As already mentioned, your arrays are not deep-cloned - use obj.map(cloneObj) instead of obj.slice().

But another oversight is typeof obj !== "object", which doesn't work for null. Better use Object(obj) !== obj.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375