Skip to content

Instantly share code, notes, and snippets.

@irfaan008
Created August 17, 2018 20:44
Show Gist options
  • Save irfaan008/1cbca107c36002ff64e09019b51bdc9b to your computer and use it in GitHub Desktop.
Save irfaan008/1cbca107c36002ff64e09019b51bdc9b to your computer and use it in GitHub Desktop.
Deep Copying javascript objects/properties
// 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