Deep copy in ES6 using the spread syntax

JavascriptEcmascript 6ReduxSpread Syntax

Javascript Problem Overview


I am trying to create a deep copy map method for my Redux project that will work with objects rather than arrays. I read that in Redux each state should not change anything in the previous states.

export const mapCopy = (object, callback) => {
    return Object.keys(object).reduce(function (output, key) {

	    output[key] = callback.call(this, {...object[key]});

	    return output;
      },
   {});
}

It works:

return mapCopy(state, e => {

    if (e.id === action.id) {
         e.title = 'new item';
    }

    return e;
})

However it does not deep copy inner items so I need to tweak it to:

export const mapCopy = (object, callback) => {
    return Object.keys(object).reduce(function (output, key) {
     
    let newObject = {...object[key]};
    newObject.style = {...newObject.style};
    newObject.data = {...newObject.data};

	output[key] = callback.call(this, newObject);

	return output;
 	}, {});
}

This is less elegant as it requires to know which objects are passed. Is there a way in ES6 to use the spread syntax to deep copy an object?

Javascript Solutions


Solution 1 - Javascript

Use JSON for deep copy

var newObject = JSON.parse(JSON.stringify(oldObject))

var oldObject = {
  name: 'A',
  address: {
    street: 'Station Road',
    city: 'Pune'
  }
}
var newObject = JSON.parse(JSON.stringify(oldObject));

newObject.address.city = 'Delhi';
console.log('newObject');
console.log(newObject);
console.log('oldObject');
console.log(oldObject);

Solution 2 - Javascript

No such functionality is built-in to ES6. I think you have a couple of options depending on what you want to do.

If you really want to deep copy:

  1. Use a library. For example, lodash has a cloneDeep method.
  2. Implement your own cloning function.
Alternative Solution To Your Specific Problem (No Deep Copy)

However, I think, if you're willing to change a couple things, you can save yourself some work. I'm assuming you control all call sites to your function.

  1. Specify that all callbacks passed to mapCopy must return new objects instead of mutating the existing object. For example:

     mapCopy(state, e => {
       if (e.id === action.id) {
         return Object.assign({}, e, {
           title: 'new item'
         });
       } else {  
         return e;
       }
     });
    

    This makes use of Object.assign to create a new object, sets properties of e on that new object, then sets a new title on that new object. This means you never mutate existing objects and only create new ones when necessary.

  2. mapCopy can be really simple now:

     export const mapCopy = (object, callback) => {
       return Object.keys(object).reduce(function (output, key) {
         output[key] = callback.call(this, object[key]);
         return output;
       }, {});
     }
    

Essentially, mapCopy is trusting its callers to do the right thing. This is why I said this assumes you control all call sites.

Solution 3 - Javascript

From MDN > Note: Spread syntax effectively goes one level deep while copying an array. Therefore, it may be unsuitable for copying multidimensional arrays as the following example shows (it's the same with Object.assign() and spread syntax).

Personally, I suggest using Lodash's cloneDeep function for multi-level object/array cloning.

Here is a working example:

const arr1 = [{ 'a': 1 }];

const arr2 = [...arr1];

const arr3 = _.clone(arr1);

const arr4 = arr1.slice();

const arr5 = _.cloneDeep(arr1);

const arr6 = [...{...arr1}]; // a bit ugly syntax but it is working!


// first level
console.log(arr1 === arr2); // false
console.log(arr1 === arr3); // false
console.log(arr1 === arr4); // false
console.log(arr1 === arr5); // false
console.log(arr1 === arr6); // false

// second level
console.log(arr1[0] === arr2[0]); // true
console.log(arr1[0] === arr3[0]); // true
console.log(arr1[0] === arr4[0]); // true
console.log(arr1[0] === arr5[0]); // false
console.log(arr1[0] === arr6[0]); // false

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.js"></script>

Solution 4 - Javascript

I often use this:

function deepCopy(obj) {
    if(typeof obj !== 'object' || obj === null) {
        return obj;
    }

    if(obj instanceof Date) {
        return new Date(obj.getTime());
    }

    if(obj instanceof Array) {
        return obj.reduce((arr, item, i) => {
            arr[i] = deepCopy(item);
            return arr;
        }, []);
    }

    if(obj instanceof Object) {
        return Object.keys(obj).reduce((newObj, key) => {
            newObj[key] = deepCopy(obj[key]);
            return newObj;
        }, {})
    }
}

Solution 5 - Javascript

const a = {
  foods: {
    dinner: 'Pasta'
  }
}
let b = JSON.parse(JSON.stringify(a))
b.foods.dinner = 'Soup'
console.log(b.foods.dinner) // Soup
console.log(a.foods.dinner) // Pasta

Using JSON.stringify and JSON.parse is the best way. Because by using the spread operator we will not get the efficient answer when the json object contains another object inside it. we need to manually specify that.

Solution 6 - Javascript

Here's my deep copy algorithm.

const DeepClone = (obj) => {
     if(obj===null||typeof(obj)!=='object')return null;
    let newObj = { ...obj };

    for (let prop in obj) {
      if (
        typeof obj[prop] === "object" ||
        typeof obj[prop] === "function"
      ) {
        newObj[prop] = DeepClone(obj[prop]);
      }
    }

    return newObj;
  };

Solution 7 - Javascript

// use: clone( <thing to copy> ) returns <new copy>
// untested use at own risk
function clone(o, m){
  // return non object values
  if('object' !==typeof o) return o
  // m: a map of old refs to new object refs to stop recursion
  if('object' !==typeof m || null ===m) m =new WeakMap()
  var n =m.get(o)
  if('undefined' !==typeof n) return n
  // shallow/leaf clone object
  var c =Object.getPrototypeOf(o).constructor
  // TODO: specialize copies for expected built in types i.e. Date etc
  switch(c) {
    // shouldn't be copied, keep reference
    case Boolean:
    case Error:
    case Function:
    case Number:
    case Promise:
    case String:
    case Symbol:
    case WeakMap:
    case WeakSet:
      n =o
      break;
    // array like/collection objects
    case Array:
      m.set(o, n =o.slice(0))
      // recursive copy for child objects
      n.forEach(function(v,i){
        if('object' ===typeof v) n[i] =clone(v, m)
      });
      break;
    case ArrayBuffer:
      m.set(o, n =o.slice(0))
      break;
    case DataView:
      m.set(o, n =new (c)(clone(o.buffer, m), o.byteOffset, o.byteLength))
      break;
    case Map:
    case Set:
      m.set(o, n =new (c)(clone(Array.from(o.entries()), m)))
      break;
    case Int8Array:
    case Uint8Array:
    case Uint8ClampedArray:
    case Int16Array:
    case Uint16Array:
    case Int32Array:
    case Uint32Array:
    case Float32Array:
    case Float64Array:
      m.set(o, n =new (c)(clone(o.buffer, m), o.byteOffset, o.length))
      break;
    // use built in copy constructor
    case Date:
    case RegExp:
      m.set(o, n =new (c)(o))
      break;
    // fallback generic object copy
    default:
      m.set(o, n =Object.assign(new (c)(), o))
      // recursive copy for child objects
      for(c in n) if('object' ===typeof n[c]) n[c] =clone(n[c], m)
  }
  return n
}

Solution 8 - Javascript

Here is the deepClone function which handles all primitive, array, object, function data types

function deepClone(obj){
	if(Array.isArray(obj)){
		var arr = [];
		for (var i = 0; i < obj.length; i++) {
			arr[i] = deepClone(obj[i]);
		}
		return arr;
	}

	if(typeof(obj) == "object"){
		var cloned = {};
		for(let key in obj){
			cloned[key] = deepClone(obj[key])
		}
		return cloned;	
	}
	return obj;
}

console.log( deepClone(1) )

console.log( deepClone('abc') )

console.log( deepClone([1,2]) )

console.log( deepClone({a: 'abc', b: 'def'}) )

console.log( deepClone({
  a: 'a',
  num: 123,
  func: function(){'hello'},
  arr: [[1,2,3,[4,5]], 'def'],
  obj: {
    one: {
      two: {
        three: 3
      }
    }
  }
}) ) 

Solution 9 - Javascript

You can use structuredClone() like the following:

const myOriginal = {
  title: "Full Stack JavaScript Developer",
  info: {
    firstname: "Abolfazl",
    surname: "Roshanzamir",
    age: 33
  }
};
const myDeepCopy = structuredClone(myOriginal);

structuredClone()

You can use structuredClone() that is a built-in function for deep-copying. Structured cloning addresses many (although not all) shortcomings of the JSON.stringify() technique. Structured cloning can handle cyclical data structures, support many built-in data types, and is generally more robust and often faster.

However, it still has some limitations that may catch you off-guard:

1-Prototypes : If you use structuredClone() with a class instance, you’ll get a plain object as the return value, as structured cloning discards the object’s prototype chain.

2-Functions: If your object contains functions, they will be quietly discarded.

3- Non-cloneables: Some values are not structured cloneable, most notably Error and DOM nodes. It will cause structuredClone() to throw.

const myDeepCopy = structuredClone(myOriginal);

JSON.stringify

If you simply want to deep copy the object to another object, all you will need to do is JSON.stringify the object and parse it using JSON.parse afterward. This will essentially perform deep copying of the object.

let user1 = {
  name: 'Abolfazl Roshanzamir',
  age: 33,
  university: {
    name: 'Shiraz Bahonar University'
  }
};


let user2 = JSON.parse(JSON.stringify(user1));

user2.name = 'Andy Madadian';
user2.university.name = 'Kerman Bahonar University'

console.log(user2);
// { name: 'Andy Madadian', age: 33, university: { name: 'Kerman Bahonar University' } }

console.log(user1);
// { name: 'Abolfazl Roshanzamir', age: 33, university: { name: 'Shiraz Bahonar University' } }

Spread operator / Object.assign()

One way to create a shallow copy in JavaScript using the object spread operator ... or Object.assign() like the following:

const myShallowCopySpread = {...myOriginal};
const myShallowCopyObjectAssign=Object.assign({},obj)

Performance

When it comes to performance the creator Surma has pointed out that JSON.Pars() can be a bit faster for small objects. But when you have a large object, complex object structuredClone() starts to get significantly faster.

Browser support is pretty fantastic And even is supported by Node.js.

Solution 10 - Javascript

function deepclone(obj) {
    let newObj = {};

    if (typeof obj === 'object') {
        for (let key in obj) {
            let property = obj[key],
                type = typeof property;
            switch (type) {
                case 'object':
                    if( Object.prototype.toString.call( property ) === '[object Array]' ) {
                        newObj[key] = [];
                        for (let item of property) {
                            newObj[key].push(this.deepclone(item))
                        }
                    } else {
                        newObj[key] = deepclone(property);
                    }
                    break;
                default:
                    newObj[key] = property;
                    break;

            }
        }
        return newObj
    } else {
        return obj;
    }
}

Solution 11 - Javascript

const cloneData = (dataArray) => {
	newData= []
	dataArray.forEach((value) => {
		newData.push({...value})
	})
	return newData
}
  • a = [{name:"siva"}, {name:"siva1"}] ;
  • b = myCopy(a)
  • b === a // false`

Solution 12 - Javascript

I myself landed on these answers last day, trying to find a way to deep copy complex structures, which may include recursive links. As I wasn't satisfied with anything being suggested before, I implemented this wheel myself. And it works quite well. Hope it helps someone.

Example usage:

OriginalStruct.deep_copy = deep_copy; // attach the function as a method

TheClone = OriginalStruct.deep_copy();

Please look at https://github.com/latitov/JS_DeepCopy for live examples how to use it, and also deep_print() is there.

If you need it quick, right here's the source of deep_copy() function:

function deep_copy() {
	'use strict';	// required for undef test of 'this' below

	// Copyright (c) 2019, Leonid Titov, Mentions Highly Appreciated.

	var id_cnt = 1;
	var all_old_objects = {};
	var all_new_objects = {};
	var root_obj = this;
	
	if (root_obj === undefined) {
		console.log(`deep_copy() error: wrong call context`);
		return;
	}

	var new_obj = copy_obj(root_obj);
	
	for (var id in all_old_objects) {
		delete all_old_objects[id].__temp_id;
	}
	
	return new_obj;
	//

	function copy_obj(o) {
		var new_obj = {};
		if (o.__temp_id === undefined) {
			o.__temp_id = id_cnt;
			all_old_objects[id_cnt] = o;
			all_new_objects[id_cnt] = new_obj;
			id_cnt ++;

			for (var prop in o) {
				if (o[prop] instanceof Array) {
					new_obj[prop] = copy_array(o[prop]);
				}
				else if (o[prop] instanceof Object) {
					new_obj[prop] = copy_obj(o[prop]);
				}
				else if (prop === '__temp_id') {
					continue;
				}
				else {
					new_obj[prop] = o[prop];
				}
			}
		}
		else {
			new_obj = all_new_objects[o.__temp_id];
		}
		return new_obj;
	}
	function copy_array(a) {
		var new_array = [];
		if (a.__temp_id === undefined) {
			a.__temp_id = id_cnt;
			all_old_objects[id_cnt] = a;
			all_new_objects[id_cnt] = new_array;
			id_cnt ++;

			a.forEach((v,i) => {
				if (v instanceof Array) {
					new_array[i] = copy_array(v);
				}
				else if (v instanceof Object) {
					new_array[i] = copy_object(v);
				}
				else {
					new_array[i] = v;
				}
			});
		}
		else {
			new_array = all_new_objects[a.__temp_id];
		}
		return new_array;
	}
}

Cheers@!

Solution 13 - Javascript

I would suggest using the spread operator. You'll need to spread a second time if you need to update the second level. Attempting to update the newObject using something like newObject.address.city will throw an error if address did not already exist in oldObject.

const oldObject = {
  name: 'A',
  address: {
    street: 'Station Road',
    city: 'Pune'
  }
}

const newObject = {
  ...oldObject,
  address: {
    ...oldObject.address,
    city: 'Delhi'
  }
}

console.log(newObject)

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionGuyView Question on Stackoverflow
Solution 1 - JavascriptNikhil MahirraoView Answer on Stackoverflow
Solution 2 - JavascriptFrank TanView Answer on Stackoverflow
Solution 3 - JavascriptMina LukeView Answer on Stackoverflow
Solution 4 - JavascriptHectorGuoView Answer on Stackoverflow
Solution 5 - JavascriptShashidhar ReddyView Answer on Stackoverflow
Solution 6 - JavascriptБектур МуратовView Answer on Stackoverflow
Solution 7 - Javascriptuser10919042View Answer on Stackoverflow
Solution 8 - Javascriptganesh phirkeView Answer on Stackoverflow
Solution 9 - JavascriptAbolfazlRView Answer on Stackoverflow
Solution 10 - JavascriptJeroen BreenView Answer on Stackoverflow
Solution 11 - JavascriptHarish SekarView Answer on Stackoverflow
Solution 12 - JavascriptlatitovView Answer on Stackoverflow
Solution 13 - JavascriptAdam YoungersView Answer on Stackoverflow