Deep copy in ES6 using the spread syntax
Asked Answered
C

15

160

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?

Candracandy answered 16/7, 2016 at 21:57 Comment(5)
See #27937272.Hydrazine
This is an XY problem. You shouldn't have to work much on deep properties in redux. instead you should just create another reducer that works on the child slice of the state shape and then use combineReducers to compose the two (or more) together. If you use idiomatic redux techniques, your problem of deep cloning objects goes away.Rimple
"Is there a way in ES6 to use the spread syntax to deep copy an object?". For the general case, impossible. Calling the spread syntax at the top any level overwrites the properties with depth that should have been merged.Abirritate
Does this answer your question? What is the most efficient way to deep clone an object in JavaScript?Exclamatory
Does this answer your question? How to Deep clone in javascriptReduce
O
90

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.

Outreach answered 17/7, 2016 at 1:21 Comment(3)
Object.assign does not deep copy objects. see developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… - Object.assign() copies property values. "If the source value is a reference to an object, it only copies that reference value."Vesiculate
Right. This is an alternative solution that does not involve deep copying. I'll update my answer to be more explicit about that.Outreach
In 2023+ we can use developer.mozilla.org/en-US/docs/Web/API/structuredClonePiglet
P
171

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);
Paraprofessional answered 7/12, 2017 at 8:18 Comment(8)
This only works if you don't need to clone functions. JSON will ignore all functions so you won't have them in the clone.Dyer
Aside from functions, you'll have issues with undefined and null using this methodPasteur
You'll also have issues with any user-defined classes, since prototype chains are not serialized.Basement
Your solution using JSON serialization has some problems. By doing this you will lose any Javascript property that has no equivalent type in JSON, like Function or Infinity. Any property that’s assigned to undefined will be ignored by JSON.stringify, causing them to be missed on the cloned object. Also, some objects are converted to strings, like Date, Set, Map, and many others.Greyhound
I was having a horrible nightmare of trying to create a true copy of an array of objects - objects which were essentially data values, no functions. If that is all you have to worry about, then this approach works beautifully.Christiansand
Plz consider adding comments info in the answer otherwise it might develop wrong base of knowledge for beginners.Marcosmarcotte
If address does not exist, and you attempt to set newObject.address.city you will get an error.Anastassia
This is such a brilliant hackParisparish
O
90

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.

Outreach answered 17/7, 2016 at 1:21 Comment(3)
Object.assign does not deep copy objects. see developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… - Object.assign() copies property values. "If the source value is a reference to an object, it only copies that reference value."Vesiculate
Right. This is an alternative solution that does not involve deep copying. I'll update my answer to be more explicit about that.Outreach
In 2023+ we can use developer.mozilla.org/en-US/docs/Web/API/structuredClonePiglet
S
41

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>
Scalar answered 4/10, 2017 at 2:34 Comment(4)
arr6 is not working for me. In browser (chrome 59.0 which supports ES6 I get Uncaught SyntaxError: Unexpected token ... and in node 8.9.3 which supports ES7 I get TypeError: undefined is not a functionat repl:1:22Ganoid
@AchiEven-dar not sire why you got an error. You may run this code directly in stackoverflow by pressing the blue button Run code snippet and it should run correctly.Scalar
arr6 is not working for me too. In browser - chrome 65Corset
arr6 might compile in some implementations, it just never does what author expected it to do.Flout
K
39

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;
        }, {})
    }
}
Kellner answered 14/12, 2018 at 0:23 Comment(1)
This function is the fastest! Result from benchmark: jsonparsejsonstringify x 5,662 ops/sec ±2.79% (91 runs sampled) structuredClone x 6,755 ops/sec ±2.76% (85 runs sampled) lodash_cloneDeep x 2,008 ops/sec ±2.74% (92 runs sampled) deepCopyFunction x 26,257 ops/sec ±2.82% (86 runs sampled) Fastest is deepCopyFunctionSweven
S
30

You can use structuredClone() like the following:

const myOriginal = {
  title: "Full Stack JavaScript Developer",
  info: {
    firstname: "Javascript",
    surname: " Addicted",
    age: 34
  }
};
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: 'Javascript Addicted',
  age: 34,
  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: 'Javascript Addicted', 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.Parse() 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.

Swaddle answered 10/5, 2022 at 17:59 Comment(2)
Unfortunately Object.assign and spread operator doesn't work when object has functions.Voight
@Vishal Kumar Sahu- It's your right. In some cases, you have to try the other ways. such as structuredClone()Swaddle
B
5
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.

Bruns answered 26/12, 2019 at 7:10 Comment(0)
T
3

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;
  };
Tyus answered 12/8, 2020 at 7:8 Comment(1)
You also need to check if 'obj[prop]!==null' as typeof(null) also return 'object'Staples
S
2
// 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
}
Snippy answered 15/1, 2019 at 20:38 Comment(1)
Comments are in the code for those looking for explanation.Hellenism
I
2

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
      }
    }
  }
}) ) 
Immersed answered 11/4, 2020 at 6:52 Comment(1)
If the Object.prototype or any other prototype object in the chain has enumerable properties your cloned object will have those properties as its own. Don't use in for an object properties iteration.Freese
M
1
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;
    }
}
Mikiso answered 8/11, 2018 at 16:22 Comment(0)
G
1
const cloneData = (dataArray) => {
    newData= []
    dataArray.forEach((value) => {
        newData.push({...value})
    })
    return newData
}
  • a = [{name:"siva"}, {name:"siva1"}] ;
  • b = myCopy(a)
  • b === a // false`
Grugru answered 6/8, 2019 at 3:28 Comment(0)
V
1

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@!

Vannesavanness answered 21/8, 2019 at 10:25 Comment(0)
A
0

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)
Anastassia answered 18/11, 2021 at 23:22 Comment(1)
this is also the recommened way for react, but i requires knowledge of the object: beta.reactjs.org/learn/…Transpose
J
0

This is a very old question but I think in 2022 there are many ways to solve this. However, if you want a simple, fast and vanilla JS solution check this out:

const cloner = (o) => {
    let idx = 1
    const isArray = (a) => a instanceof Array
    const isObject = (o) => o instanceof Object
    const isUndefined = (a) => a === undefined
    const process = v => {
        if (isArray(v)) return cloneArray(v)
        else if (isObject(v)) return cloneObject(v)
        else return v
    }
    const register = (old, o) => {
        old.__idx = idx
        oldObjects[idx] = old
        newObjects[idx] = o
        idx++
    }
    const cloneObject = o => {
        if (!isUndefined(o.__idx)) return newObjects[o.__idx]

        const obj = {}
        for (const prop in o) {
            if (prop === '__idx') continue
            obj[prop] = process(o[prop])
        }
        register(o, obj)

        return obj
    }
    const cloneArray = a => {
        if (!isUndefined(a.__idx)) return newObjects[a.__idx]

        const arr = a.map((v) => process(v))
        register(a, arr)

        return arr
    }
    const oldObjects = {}
    const newObjects = {}

    let tmp
    if (isArray(o)) tmp = cloneArray(o)
    else if (isObject(o)) tmp = cloneObject(o)
    else return o

    for (const id in oldObjects) delete oldObjects[id].__idx

    return tmp
}

const c = {
  id: 123,
  label: "Lala",
  values: ['char', 1, {flag: true}, [1,2,3,4,5], ['a', 'b']],
  name: undefined
}

const d = cloner(c)
d.name = "Super"
d.values[2].flag = false
d.values[3] = [6,7,8]
console.log({ c, d })

It's recursive and self-contained, all the functions needed are defined in the function cloner().

In this snippet we are handling Array and Object types if you want to add more handlers you can add specify handlers like Date and clone it like new Date(v.getTime())

For me Array and Object are the types that I use the most in my implementations.

Jermaine answered 16/9, 2022 at 9:25 Comment(0)
V
0

Here is my approach for DeepCopy/DeepMerge to overcome limitations of Object.assign for the objects with function.


Helper = {
  isObject(obj) {
    return obj !== null && typeof obj === 'object';
  },

  isPlainObject(obj) {
    return Helper.isObject(obj) && (
      obj.constructor === Object  // obj = {}
        || obj.constructor === undefined // obj = Object.create(null)
    );
  },

  mergeDeep(target, ...sources){
    if (!sources.length) return target;
    const source = sources.shift();

    if (Helper.isPlainObject(source) || Array.isArray(source)) {
      for (const key in source) {
        if (Helper.isPlainObject(source[key]) || Array.isArray(source[key])) {
          if (Helper.isPlainObject(source[key]) && !Helper.isPlainObject(target[key])) {
            target[key] = {};
          }else if (Array.isArray(source[key]) && !Array.isArray(target[key])) {
            target[key] = [];
          }
          Helper.mergeDeep(target[key], source[key]);
        } else if (source[key] !== undefined && source[key] !== '') {
          target[key] = source[key];
        }
      }
    }

    return Helper.mergeDeep(target, ...sources);
  },
}

Voight answered 5/5, 2023 at 11:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.