Created
August 17, 2018 20:44
-
-
Save irfaan008/1cbca107c36002ff64e09019b51bdc9b to your computer and use it in GitHub Desktop.
Deep Copying javascript objects/properties
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Problem Statement : | |
// Write a function called deepClone which takes an object and creates a copy of it. | |
// e.g. {name: "Paddy", address: {town: "Lerum", country: "Sweden"}} -> {name: "Paddy", address: {town: "Lerum", country: "Sweden"}} | |
// Solution | |
// This file shows ability of multiple approach with their advantages and disadvantages and 2 fully tested and working approach. | |
// One has been written by me and another has been received from community to showcase that self written code is more concise and readable. | |
// Just run this file in Node Js environment to get the result. | |
// To run test cases, un comment method named test() in the bottom of file | |
// Using json parser | |
// 5 out 35 test cases failed | |
// Date and user defined function fails in this approach | |
// Not for production use | |
const deepClone1 = object => { | |
if (null == object || typeof object != "object") return object; | |
return JSON.parse(JSON.stringify(object)); | |
} | |
// Using ES6 create syntax | |
// 14 out 35 test cases failed | |
// creates prototypal references instead of actual clonning of object thus changing properties of main object gets reflected on the clone one | |
// Not for production use | |
const deepClone2 = object => { | |
if (null == object || typeof object != "object") return object; | |
return Object.create(object); | |
} | |
// Using ES6 assign syntax | |
// 10 out 35 test cases failed | |
// It makes references of nested objects and thus fails in most of cases | |
// Not for production use | |
const deepClone3 = object => { | |
if (null == object || typeof object != "object") return object; | |
return Object.assign({}, object); | |
} | |
// Using ES6 assign syntax | |
// All 35 test cases passed | |
// It handles date, object and array differently thus making it ideal solution for production use | |
// For produciton use | |
const deepClone = (object) => { | |
if (null == object || typeof object != "object") return object; | |
// Handle Date | |
if (object instanceof Date) { | |
var copy = new Date(); | |
copy.setTime(object.getTime()); | |
return copy; | |
} | |
if (object instanceof Array) { | |
return object.map(item => deepClone(item)) | |
} | |
var newObject = {}; | |
for (var key in object) { | |
if (typeof object[key] === 'object') { | |
newObject[key] = deepClone(object[key]); | |
} else { | |
newObject[key] = object[key]; | |
} | |
} | |
return newObject; | |
} | |
// This code I got with the curiosity in mind to know how others have done. | |
// This also passes all 35 cases but code written by me is more concise and readable | |
// Not for production use | |
function deepCloneFromCommunity(object) { | |
if (null == object || typeof object != "object") return object; | |
var clonedObjectsArray = []; | |
var originalObjectsArray = []; //used to remove the unique ids when | |
var next_objid = 0; | |
function objectId(object) { | |
if (object == null) return null; | |
if (object.__obj_id == undefined) { | |
object.__obj_id = next_objid++; | |
originalObjectsArray[object.__obj_id] = object; | |
} | |
return object.__obj_id; | |
} | |
function cloneRecursive(object) { | |
if (null == object || typeof object == "string" || typeof object == "number" || typeof object == "boolean") return object; | |
// Handle Date | |
if (object instanceof Date) { | |
var copy = new Date(); | |
copy.setTime(object.getTime()); | |
return copy; | |
} | |
// Handle Array | |
if (object instanceof Array) { | |
var copy = []; | |
for (var i = 0; i < object.length; ++i) { | |
copy[i] = cloneRecursive(object[i]); | |
} | |
return copy; | |
} | |
// Handle Object | |
if (object instanceof Object) { | |
if (clonedObjectsArray[objectId(object)] != undefined) | |
return clonedObjectsArray[objectId(object)]; | |
var copy; | |
if (object instanceof Function)//Handle Function | |
copy = function () { return object.apply(this, arguments); }; | |
else | |
copy = {}; | |
clonedObjectsArray[objectId(object)] = copy; | |
for (var attr in object) | |
if (attr != "__obj_id" && object.hasOwnProperty(attr)) { | |
copy[attr] = cloneRecursive(object[attr]); | |
} | |
return copy; | |
} | |
throw new Error("Unable to clone object! Its type isn't supported."); | |
} | |
var cloneObj = cloneRecursive(object); | |
for (var i = 0; i < originalObjectsArray.length; i++) { | |
delete originalObjectsArray[i].__obj_id; | |
}; | |
return cloneObj; | |
} | |
// Common logging. Allows disabling the same if required | |
const log = (message, shouldForce) => { | |
if (shouldForce === true) { | |
console.log(message); | |
return; | |
} | |
// Disable log if required. Only test result will be displayed | |
if (process.env.NODE_ENV != 'production') | |
console.log(message); | |
} | |
// Helper method to print input and output for all test cases to understand the behaviour manually | |
const deepCloneHelper = (testCase) => { | |
log("==>input"); | |
log(testCase); | |
var clonedObj = deepCloneFromCommunity(testCase); | |
log("==> Cloned"); | |
log(clonedObj); | |
return clonedObj; | |
} | |
// Main method to execute the provided test case | |
const run = () => { | |
var testCase = { name: "Paddy", address: { town: "Lerum", country: "Sweden" } }; | |
var deepClonedCopy = deepCloneHelper(testCase); | |
testCase.address.country = "India"; | |
log("==> Cloned object after altering original input"); | |
log(deepClonedCopy); | |
} | |
// Test cases along with its implementation without any third party library | |
const test = () => { | |
var passed = 0; | |
var failed = 0; | |
const assert = condition => { | |
if (!condition) { | |
log("test failed\n") | |
failed++; | |
} else | |
log("test passed\n") | |
passed++; | |
} | |
// Test cases | |
var tc0 = [1, 2, 3]; | |
// Testcase 1 : Nested objects | |
var tc1 = { name: "Paddy", address: { town: "Lerum", country: "Sweden" } }; | |
// Testcase 2 : Date object with time | |
var tc2 = new Date(); | |
// Testcase 3 : Undefined and Infinitey in object | |
var tc3 = { b: Infinity, c: undefined }; | |
// Testcase 4 : User funciton in object | |
var tc4 = { | |
name: 'Object with function', | |
getName: function () { | |
return this.name; | |
} | |
} | |
// Testcase 5 : Cyclic reference object | |
var tc5 = { | |
a: 'a', | |
b: { | |
c: 'c', | |
d: 'd', | |
}, | |
}; | |
// Testcase 6 : Array with nested objects | |
var tc6 = [ | |
"a", | |
"c", | |
"d", { | |
four: 4 | |
}, | |
]; | |
// This tests demonstrate whether cloned object is similar to the original one or not | |
const testClonedObjectQuality = () => { | |
// Basic tests | |
assert(deepCloneHelper(null) === null) | |
assert(deepCloneHelper(undefined) === undefined) | |
assert(deepCloneHelper() === undefined) | |
assert(deepCloneHelper(1) === 1) | |
assert(deepCloneHelper("abc") === "abc") | |
try { | |
assert(deepCloneHelper(() => "abc")() === "abc") | |
} catch (e) { | |
assert(false); | |
} | |
{ | |
var deepCloneCopy = deepCloneHelper(tc0); | |
assert(JSON.stringify(deepCloneCopy) === JSON.stringify(tc0)) | |
assert(deepCloneCopy[0] === 1) | |
assert(deepCloneCopy[1] === 2) | |
assert(deepCloneCopy[2] === 3) | |
log("Len : " + deepCloneCopy.length); | |
assert(deepCloneCopy.length === 3) | |
log(JSON.stringify(deepCloneCopy)); | |
} | |
// Advanced tests | |
// Note : There is no native way of checking equality of two objects. Converting it into string for check | |
{ | |
var deepCloneCopy = deepCloneHelper(tc1); | |
assert(JSON.stringify(deepCloneCopy) === JSON.stringify(tc1)) | |
assert(Object.keys(deepCloneCopy).length === Object.keys(tc1).length); | |
assert(deepCloneCopy.address.country === tc1.address.country); | |
} | |
{ | |
var deepCloneCopy = deepCloneHelper(tc2); | |
try { | |
assert(JSON.stringify(deepCloneCopy) === JSON.stringify(tc2)) | |
} catch (e) { | |
assert(false); | |
} | |
try { | |
assert(deepCloneCopy.getDate() === tc2.getDate()); | |
} catch (e) { | |
assert(false); | |
} | |
try { | |
assert(deepCloneCopy.getMilliseconds() === tc2.getMilliseconds()); | |
} catch (e) { | |
assert(false); | |
} | |
} | |
{ | |
var deepCloneCopy = deepCloneHelper(tc3); | |
assert(deepCloneCopy.b === tc3.b); | |
assert(deepCloneCopy.c === tc3.c); | |
} | |
{ | |
var deepCloneCopy = deepCloneHelper(tc4); | |
try { | |
assert(deepCloneCopy.getName() === tc4.getName()); | |
} catch (e) { | |
assert(false); | |
} | |
} | |
{ | |
var deepCloneCopy = deepCloneHelper(tc6); | |
assert(JSON.stringify(deepCloneCopy) === JSON.stringify(tc6)) | |
assert(Object.keys(deepCloneCopy).length === Object.keys(tc6).length); | |
assert(deepCloneCopy[3].four === tc6[3].four); | |
} | |
} | |
// This tests demonstrate whether cloned object is the new copy (in memory) or referencing to the original one | |
const testImmutability = () => { | |
{ | |
var deepCloneCopy = deepCloneHelper(tc0); | |
deepCloneCopy[0] = 2 | |
assert(deepCloneCopy[0] === 2) | |
assert(tc0[0] === 1) | |
tc0[1] = 4; | |
assert(deepCloneCopy[1] === 2) | |
assert(tc0[1] === 4) | |
} | |
{ | |
var deepCloneCopy = deepCloneHelper(tc1); | |
tc1.address.country = "India"; | |
assert(tc1.address.country === "India"); | |
assert(deepCloneCopy.address.country === "Sweden"); | |
} | |
{ | |
var deepCloneCopy = deepCloneHelper(tc2); | |
tc2.setDate(tc2.getDate() + 1); | |
var today = new Date(); | |
var tomorrow = new Date(); | |
tomorrow.setDate(tomorrow.getDate() + 1); | |
try { | |
assert(deepCloneCopy.getDate() === today.getDate()); | |
} catch (e) { | |
assert(false); | |
} | |
try { | |
assert(tc2.getDate() === tomorrow.getDate()); | |
} catch (e) { | |
assert(false); | |
} | |
} | |
{ | |
var deepCloneCopy = deepCloneHelper(tc5); | |
// Simulating cyclic reference | |
tc5.c = tc5.b; | |
tc5.e = tc5.a; | |
tc5.b.c = tc5.c; | |
tc5.b.d = tc5.b; | |
tc5.b.e = tc5.b.c; | |
log("After cyclic rotation"); | |
log(tc5); | |
assert(deepCloneCopy.b.c !== tc5.b.c); | |
assert(deepCloneCopy.e !== tc5.e); | |
} | |
{ | |
var deepCloneCopy = deepCloneHelper(tc6); | |
tc6[0] = 'b'; | |
tc6[3].four = 5; | |
log("After cyclic rotation"); | |
log(tc6); | |
assert(deepCloneCopy[3].four === 4); | |
assert(tc6[3].four === 5); | |
} | |
} | |
testClonedObjectQuality() | |
testImmutability() | |
log("Failed test cases: " + failed, true); | |
log("Passed test cases : " + passed, true); | |
} | |
run(); | |
// test(); | |
// #### Conclusion #### | |
// At first it looked pretty simpler but when I started testing it with multiple data sets, I found the advantages/disadvantages of various methods. | |
// Few approaches | |
// 1. Stringifying input object and parsing it back - This works in most of the case. This doesn't work for infinity and undefined type of values and simply ignores that. | |
// Also this doesn't seems to be performance optimised for deep cloning (as the question is intended to do that only). On further testing this doesn't work | |
// for object with function as well. Doesn't support circular objects. | |
// 2. Object.create() creates prototypal references instead of actual clonning of object thus changing properties of main object gets reflected on the clone one | |
// 3. I am big fan of Underscore and they have _.clone for this type of task but considering the scope of work without using any third party library, | |
// I am ignoring this solution. Similary Lodash has _.clonedeep method. | |
// 4. The ES6 solution using Object.assign works pretty well where JSON.parse doesn't work but fails for nested objects. | |
// Here is the warning from Mozilla on using this - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Deep_Clone | |
// 5. The ES6 spread syntax doesn't work well with circular dependencies and can not handle date type | |
// 6. There is no inbuilt way to perform this operation and hence a hybrid code has to be written to acheive the desired result. In this case | |
// method named deepClone provides the desired result for all 35 test cases. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment