1

If I wanted to clone any javascript object (that's not null), I would think I could just copy all of its own properties (enumerable and non-enumerable) -- using Object.getOwnPropertyNames -- onto a new empty object.

But I've noticed that an example of a deep cloning function provided by Dojo toolkit (https://davidwalsh.name/javascript-clone) treats RegExp, Date, and Node objects as special cases, and lodash.cloneDeep also has a lot of logic that is a lot more complicated than simply copying properties, including having some special cases of its own and apparently not supporting all types of objects: (https://github.com/lodash/lodash/blob/master/.internal/baseClone.js).

Why is simply copying the object properties not sufficient? What else is there to a javascript object besides its properties that I don't know about?

EDIT: to be clear, I'm talking about deep cloning an object. Sorry for the confusion.

dsto
  • 21
  • 4
  • Possible duplicate of [How do I correctly clone a JavaScript object?](https://stackoverflow.com/questions/728360/how-do-i-correctly-clone-a-javascript-object) – Jared Smith Sep 16 '18 at 20:15
  • Also: https://stackoverflow.com/questions/122102/what-is-the-most-efficient-way-to-deep-clone-an-object-in-javascript – brass monkey Sep 16 '18 at 20:16
  • You're looking for [`Object.assign()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) to get all the enumerable properties anyway. – Scott Marcus Sep 16 '18 at 20:16
  • Prototype properties. Built in properties not accessible via `ownPropertyNames/Symbols`. etc, etc. Trying to to compare objects for equality is similarly [complicated](https://gist.github.com/jasmith79/3fa6e391f47581823bcf04f461ec4c7c). – Jared Smith Sep 16 '18 at 20:16
  • Object.assign will only copy the top level properties and is not a clone, the immutability of the original object is not maintained if any reference objects were copies. – Adrian Brand Sep 16 '18 at 20:50
  • @JaredSmith The answer in your provided link appears to be 9 years old and doesn't really answer my question about why Date, RegExp, Node, etc should be treated differently (and raises many questions of its own). I don't understand what you are saying about prototype properties causing a problem. Can't I just create my new object to have the same prototype as the source object? As for built in properties, can you tell me why I need to worry about cloning those? – dsto Sep 27 '18 at 00:21
  • @dsto I've retracted my close vote and posted an answer. – Jared Smith Sep 27 '18 at 11:11

5 Answers5

1

If the top level properties are all value objects like strings and numbers then just copying the top level properties is fine for a clone of an object. If there are any reference objects such as dates, arrays or other objects then all your are doing is copying a reference from one object to another. If you change the reference object on the clone you will mutate the original object.

Take a look at my clone function at https://stackblitz.com/edit/typescript-qmzgf7

If it is an array it clones every item in the array, if it is a date it creates a new date with the same time, if it is an object it clones every property else if just copies the property.

The cloned object can now be mutated without worrying about effects it might have on the original object.

const clone = obj =>
  Array.isArray(obj)
    ? obj.map(item => clone(item))
    : obj instanceof Date
      ? new Date(obj.getTime())
      : (typeof obj === 'object') && obj
        ? Object.getOwnPropertyNames(obj).reduce((o, prop) => ({ ...o, [prop]: clone(obj[prop]) }), {})
        : obj;
        
let original = { prop1: "Original", objProp: { prop1: "Original" } };
let swallowCopy = { ...original };
let clonedObj = clone(original);

clonedObj.prop1 = "Changed";
clonedObj.objProp.prop1 = "Changed";

console.log(`Original objects properties are '${original.prop1}' and '${original.objProp.prop1}'`);

swallowCopy.prop1 = "Changed";
swallowCopy.objProp.prop1 = "Changed";

console.log(`Original objects properties are '${original.prop1}' and '${original.objProp.prop1}'`);

Notice how modifying the property on the object property shallow copy causes the original to change as well.

Adrian Brand
  • 20,384
  • 4
  • 39
  • 60
  • This doesn't even come close to being sufficient. What about RegExp, Symbol, cross-realm constructors, null, HTMLElements, boxed primitives (e.g. `new String('foo')`), etc. etc. – Jared Smith Sep 21 '18 at 18:37
  • Considering that 100% of the time I use it I am cloning data that came from a server in JSON format it is fine. I use it so that we can clone an object for 2 way bind to views and protect the immutability of the data in the store. – Adrian Brand Sep 21 '18 at 20:38
0

The easiest way to clone an object in JS is by using the ... spread operator.

Let's say you have this object:

const object = { foo: 1, bar: 2 }

To clone it, you can simply declare:

const objectClone = {...object}.

This will create all the properties present in the original object onto the clone, as well as their values.

Now the problem is, if you have any object nested in there, the copies will be made by reference. Suppose the original object is this instead:

const student = { studentID: 1, tests: { test1: 90, test2: 95}}

If you create a copy of that object by using the spread operator(or Object.assign, spread is just syntactic sugar), the nested object will actually point to the object inside the original object! So repeating this:

const studentClone = {...student}

And now you edit a property of the nested object inside the clone:

studentClone.tests.test1 = 80

This will change the value in both clone, and original object, as the nested object is really just pointing to 1 object in memory.

Now what those utilities, like _.cloneDeep will do, is iterate through all inner objects in the object you're cloning, and repeat the process. You could technically do it yourself, but you wouldn't be able to do it on objects with many nested objects easily. Something like this:

const studentClone = {...studentClone, tests: {...studentClone.tests}}

This would create new objects, with no reference problems.

Hope this helped!

EDIT: Just adding, object spreading would only work properly for prototype objects, of course. Each instantiated objects,such as arrays, Date objects etc, would have their own way of cloning.

Arrays can be copied similarly, through [...array]. It does follow the same rules regarding to references. For dates, you can simply pass the original date object into the Date constructor again:

const clonedDate = new Date(date)

This is where the third-party utilities will come in handy, as they'll usually handle most use cases.

  • This doesn't even come close to being sufficient. What about RegExp, Symbol, cross-realm constructors, null, HTMLElements, boxed primitives (e.g. `new String('foo')`), etc. etc. – Jared Smith Sep 21 '18 at 18:41
  • It sure doesn't! That's why _.cloneDeep and the likes are still being used. But that doesn't make it an invalid strategy when it's adequate, e.g.redux reducers in most cases, shallow comparisons, etc – Bernardo Siqueira Sep 21 '18 at 23:27
0

This answer does a good job of explaining two of the problems with cloning a normal JavaScript object: prototype properties and circular references. But to answer your question regarding certain built-in types, the TL;DR answer is that there are 'under the hood' properties that you have no programmatic access to.

Consider:

let foo = [1, 2];
let bar = {};
Object.assign(bar, foo);
Object.setPrototypeOf(bar, foo.constructor.prototype); // aka Array.prototype
bar[0]; // 1
bar instanceof Array; // true
bar.map(x => x + 1); // [] ????

Empty array? Why? Just to make sure we're not crazy

foo.map(x => x + 1); // [2, 3]

The reason why map (and the other array methods) fail to work is that an Array isn't simply an object: it has internal slot properties for the stuff you put in it that you don't get to see as the JavaScript programmer. As another example, every JavaScript object has an internal [[Class]] property that says what kind of object it is. Fortunately for us, there's a loophole in the spec that allows us indirect access to it: the good ol Object.prototype.toString.call hack. So let's see what that has to say about various stuff:

 Object.prototype.toString.call(true);   // [object Boolean]
 Object.prototype.toString.call(3);      // [object Number]
 Object.prototype.toString.call({});     // [object Object]
 Object.prototype.toString.call([]);     // [object Array]
 Object.prototype.toString.call(null);   // [object Null]
 Object.prototype.toString.call(/\w/);   // [object RegExp]
 Object.prototype.toString.call(JSON);   // [object JSON]
 Object.prototype.toString.call(Math);   // [object Math]

Let's see what it says about our foo and bar:

 Object.prototype.toString.call(foo); // [object Array]
 Object.prototype.toString.call(bar); // [object Object] Doh!

There's no way to 'convert' a random object to an Array... or a Date... or an HTMLElement... or a regex. Now, there are in fact ways to clone all of those things, but they require special logic: you can't just copy properties, or even set the prototype, because they have internal logic you can't access or directly replicate.

In normal everyday JavaScript programming we don't worry too much about this stuff, it's the kind of thing that's generally of interest to library authors (or language implementers). We everyday working stiffs just use a library to cover the edge cases and call it a day. But every once in a while the abstractions we use leak and the ugly bubbles through. This is however a great illustration of why you should probably use battle-tested libraries rather than trying to roll your own.

Jared Smith
  • 19,721
  • 5
  • 45
  • 83
-1

An object in javascript includes fields and functions together, and every field could be another object (Like Date type). If you copy a date field, it will be a reference type assignment. Example:

var obj1 = { myField : new Date('2018/9/17') };

var obj2 = {};
obj2.myField = obj1.myField;

Now, if we change "obj2.myField" like this:

obj2.myField.setDate(obj2.myField.getDate() + 2);

console.log(obj1.myField);   // Result =====>  Wed Sep 19 2018 00:00:00 GMT+0430

As you see, obj1 and obj2 still are linked.

Correct way to copy a date field:

obj2.myField = new Date(obj1.myField.getTime());
Ramin
  • 134
  • 6
  • This does not answer the question. – Jared Smith Sep 19 '18 at 22:29
  • Question: "Why is simply copying the object properties not sufficient?" I answered this question. – Ramin Sep 21 '18 at 18:32
  • This adds absolutely nothing to skyboyer's answer (posted almost an hour before yours) which is, itself, insufficient: even if all (visible, enumerable) properties of an object are scalars you *still* can't clone it naively. – Jared Smith Sep 21 '18 at 18:39
-2

Most native objects(like you have mentioned - I don't know for is the correct naming for them; maybe built-in?) are treated as "simple": it does not make sense to copy Date object property-by-property. In the same time they all are mutable in some way.

let a = {test: new Date(1)}; // test === Thu Jan 01 1970 00:00:00GMT
let copy_a = {test: a.test}; // looks like cloned
a.test.setDate(12); // let's mutate original date
console.log(copy_a.test); // Thu Jan 12 1970 00:00:00GMT ooops modified as well

So you either should handle that exceptions(special cases) explicitly or take a risk of side effects for some cases.

skyboyer
  • 22,209
  • 7
  • 57
  • 64
  • 1
    1. It doesn't answer the question, which is why simply copying `ownPropertyNames` is insufficient. 2. The question already has an answer, please refrain from answering duplicates: flag for closure instead. 3. You make a big deal about the difference between mutable reference and immutable value types, but that wasn't the gist of the question. 4. What exceptions (sic)? – Jared Smith Sep 19 '18 at 22:33
  • Thank you Jared Smith for explaining. I see part about special cases(exceptions, exceptional cases) with Regexp, Date, Node objects in the question. – skyboyer Sep 20 '18 at 06:19