DEV Community

Peter Tasker
Peter Tasker

Posted on

Best way to copy an object in JavaScript?

Object.assign()

So I'm always looking for a way to use vanilla JS whenever possible these days, and I discovered that deep copying an object in JavaScript is still weird.

StackOverflow reminded me of the JSON.parse( JSON.stringify( obj ) ) trick, and it looks like Object.assign still doesn't copy nested objects.

jQuery's $.extend() works. But that's not vanilla JS any more.

What hacks do you guys use for copying JS objects?

ยฏ\_(ใƒ„)_/ยฏ

Top comments (18)

Collapse
 
francormm profile image
francoRMM

I use spread to copy and assign a new value to the old object

const copy=old=>{
  return {...old};
}

const add=(old,val)=>{
  return {...old,...val};
}

but this is the same as Object.assign({}, obj);, is not a deep copy.

Collapse
 
hosembafer profile image
Rafael Hovhannisyan • Edited

In plain javascript, there is only one option.

JSON.parse( JSON.stringify( obj ) );
Enter fullscreen mode Exit fullscreen mode

I don't know how and why, but in some cases Object.assign is working, but it's not recommended for use without deep understanding of it.

Collapse
 
isaacdlyman profile image
Isaac Lyman

This is great for simple object literals. But, if someone out there tries it and it doesn't work, allow me to offer some reasons why.

  • JSON.parse will fail for any object that recurses on itself. Try this in your console: var a = {}; a.a = a; JSON.stringify(a);. This is uncommon in app-level code, but happens a lot in DOM parsing libraries and frameworks.
  • This method doesn't allow you to make "shallow copies" (copies of the immediate properties of the object, without going any deeper). jQuery and Lodash, on the other hand, have options that allow you to do this.
  • Objects that have a toJSON() method will not truly be copied as-is. The toJSON() method will be invoked and its response will be assumed to be correct JSON, whether or not it corresponds to the actual properties of the object.
  • Methods will not be copied.
  • Dates will be parsed as Strings, not as Dates.

To cover your own butt, you're better off doing as others have suggested and using jQuery or Lodash. In modern ES6 code, you don't have to worry about code bloat, since you can just import the method you want to use from either toolkit and the rest of the library won't get included in your browser bundle.

Collapse
 
jvanbruegge profile image
Jan van Brรผgge • Edited

I would simply use recursion for a deep copy:

function deepCopy(obj) {
 if(typeof obj === 'object') {
  return Object.keys(obj)
   .map(k => ({ [k]: deepCopy(obj[k]) }))
   .reduce((a, c) => Object.assign(a, c), {});
 } else if(Array.isArray(obj)) {
  return obj.map(deepCopy)
 }
 return obj;
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
foresthoffman profile image
Forest Hoffman • Edited

Well I suppose it depends on how deep the objects are and whether you have control over the organization of the objects. I believe for simple scenarios, you could do something along the following. Dunno if you qualify ES6 as Vanilla JS, so here's some ES5:

function Garage(x, y, cars) {
  // dimensions of the garage in feet, because America :/
  this.area = {
    width: x,
    depth: y,
  }
  this.cars = cars;

  this.copy = function(garage){
    this.area.width = garage.area.width;
    this.area.depth = garage.area.depth;
    for (var i = 0; i < garage.cars.length; i++) {
      this.cars[i] = new Car(garage.cars[i].make, garage.cars[i].model);
    }
  };
}

function Car(make, model) {
  this.make = make;
  this.model = model;
}

(function(){
  var someGarage = new Garage(40.0, 25.0, [
    new Car("Toyota", "Camry"),
    new Car("Toyota", "Prius"),
    new Car("Tesla", "Model 3"),
  ]);
  var otherGarage = new Garage(25.0, 25.0, [
    new Car("Toyota", "Camry"),
  ]);

  // make the second garage a copy of the first, without using references
  otherGarage.copy(someGarage);

  // change the first garage to show that the garages aren't related
  someGarage.cars[2] = null;
  someGarage.area.width++;

  console.log("someGarage: ", someGarage);
  console.log("otherGarage: ", otherGarage);
})();

The output to the console:

someGarage:  {โ€ฆ}
    area: Object { width: 41, depth: 25 }
    cars: [โ€ฆ]
        0: Object { make: "Toyota", model: "Camry" }
        1: Object { make: "Toyota", model: "Prius" }
        2: null
        length: 3
        __proto__: Array []
    copy: function Garage/this.copy()
    __proto__: Object { โ€ฆ }

otherGarage:  {โ€ฆ}
    area: Object { width: 40, depth: 25 }
    cars: [โ€ฆ]
        0: Object { make: "Toyota", model: "Camry" }
        1: Object { make: "Toyota", model: "Prius" }
        2: Object { make: "Tesla", model: "Model 3" }
        length: 3
        __proto__: Array []
    copy: function Garage/this.copy()
    __proto__: Object { โ€ฆ }

Alternatively, if you have to handle dynamic objects of unknown dimensions, you could use a recursive function that uses Object.getOwnPropertyNames and the prototypes (someObject.__proto__) of those properties to handle the generation of fresh Objects for the properties of the destination object.

Hope that answers your question!

Edit: Added console output for the above script.

Collapse
 
ptasker profile image
Peter Tasker

That does indeed look like a good solution if you know the object props.

But JSON.parse( JSON.stringify( obj ) );, though hacky, is much quicker to write!

Collapse
 
foresthoffman profile image
Forest Hoffman

Oh okay. I misunderstood, I thought you were avoiding using that JSON.parse( JSON.stringify( obj ) ); for some reason. Whoops!

Have a good Turkey day, or a good Thursday.

Collapse
 
ptasker profile image
Peter Tasker

Good to see I'm not crazy! JSON.parse( JSON.stringify( obj ) ); seems to be a common 'shortcut'. But assigning each prop to a new object with obj.hasOwnProperty(item) also looks like a good option if you know which properties to look for.

Collapse
 
bgadrian profile image
Adrian B.G.

I'm all in for vanilla JS but to a point, I would suggest copy the function from Lodash or jQuery. Use the code that was written and tested over the years, circular references can be a pain in the ..code.

Collapse
 
lexlohr profile image
Alex Lohr • Edited

Good ol' var copy = {}; for (var item in obj) { obj.hasOwnProperty(item) && (copy[item] = obj[item]); } approach works most of the times.

About those cases when this doesn't work: maybe you're solving the wrong problem.

Collapse
 
maginot_ profile image
Maginot Junior

Oh boy...
I saw a lot of comments, many with arguments like "if you do this it doesn't work". Well, please note that isn't a "not working" subject but BY DESIGN. Deep copy will not keep references for example, and thus will not be carried along. Shallow copies will keep references and so can make shit happens if you're not aware of its behaviour. It's a matter of understanding exactly what you need and what you're cloning/copying.
Oh and BTW, undefined isn't equal to null. Undefined is a global object with a primitive "undefined" value, when setting something to undefined you are setting a reference to the undefined global object and a deep copy will not carry it along just like it wouldn't with any other references.
Keep that in mind.

Collapse
 
xenon15 profile image
Ashwani Sindhu

I use this utility function for deep cloning the objects in vanilla JavaScript. It handles string properties, symbol properties and circular references in the input object.

function cloneDeep(data, map = new Map()) {
  if (data === undefined || data === null || typeof data !== 'object') {
    return data;
  }
  if (map.has(data)) {
    return map.get(data);
  }
  const result = Array.isArray(data) ? [] : {};
  map.set(data, result);
  const allKeys = [...Object.keys(data), ...Object.getOwnPropertySymbols(data)]
  for (const key of allKeys) {
      result[key] = cloneDeep(data[key], map);
  }

  return result;
}

Enter fullscreen mode Exit fullscreen mode
Collapse
 
nektro profile image
Meghan (she/her)

So you're saying Object.assign({}, obj); doesn't work?

Collapse
 
gobeli profile image
Etienne • Edited

It does work, but not for nested objects. i.e.:

const x = {
  foo: {
    bar: false
  }
}
const y = Object.assign({}, x);
console.log(x === y); // outputs false
console.log(x.foo === y.foo); // outputs true
Collapse
 
ptasker profile image
Peter Tasker

Yeah it doesn't work on nested objects.