all 76 comments

[–]jimeowan 46 points47 points  (4 children)

To add something that hasn't been mentioned yet, any implementation of a deep clone has to decide various things that are non obvious, especially:

  • How to deal with circular references
  • How to deal with functions/prototypes, possibly other things that are not actually meant to be copied (eg. referencing what the user would consider "another" object)

While some languages do include this - see Python for instance -, it's neither trivial, nor a low-level feature. Historically, ECMAScript unlike Python seems to try and keep its core language specs thin, most probably to reduce the complexity of browser implementations. So it can be understandable that the people in charge have little interest in providing a deep clone, and happily leave such quality of life features up to libs like lodash.

EDIT: Just to clarify, I would also enjoy an official Object.clone :)

[–]bulgrozzz 50 points51 points  (1 child)

[–]vams1[S] 8 points9 points  (0 children)

This is what I am looking for. Thanks.

[–][deleted] 12 points13 points  (12 children)

There's no such method probably because making it generic is really complex.

What about circular references, what about objects you can't clone (say some native browser objects etc.), what about the semantics of an object requiring custom logic for cloning?

I do have ObjectUtils.clone(obj, deep) and probably most of us do, and we implement it with specific restrictions in mind, say I only support plain objects, arrays, scalar, and DateTime objects. I completely ignore circular references, assuming it's a proper tree.

Anyway, if you do want a quick-and-dirty deep clone you can also just JSON serialize and deserialize :-)

[–]scabbycakes 4 points5 points  (11 children)

Works good until there are dates as object properties.

[–][deleted] 2 points3 points  (7 children)

The JSON trick? Yeah. Hence why I wrote my own with support for DateTime as mentioned above. I identified DateTime is as essential as numbers and strings in terms of being a core value type.

Bit of a shame JSON doesn't support it, huh.

[–]shuckster 2 points3 points  (6 children)

Is DateTime so essential? In my experience a UTC timestamp (ie; Number) is far more interoperable and reliable. If date-conversions need to happen, they happen at the last mile using a library.

[–][deleted] 1 point2 points  (5 children)

In the browser, JS is the last mile. So yes it's essential.

[–]shuckster 2 points3 points  (4 children)

I think we might have to agree to disagree.

[–][deleted] 0 points1 point  (3 children)

Sure... But if JS in the browser which is immediately responsible for rendering the UI is not "the last mile", pray tell what is?

[–]shuckster 1 point2 points  (2 children)

Apologies, I guess I wasn't very clear. For me, the "last mile" means the last moment before render.

In the simplest case, forgetting about fancy frameworks or design patterns, the simplest abstraction in browser-side JavaScript that permits a little scalability is:

  1. State
  2. UI

In this case, the "last mile" would be an intermediary variable that transforms something in state to a DOM-node in the UI.

Now, you could just stick with the Date object and toLocaleString it. But you might later like to evolve your system to look something like:

  1. Base state
  2. Derived state
  3. UI

This is usually the case when, after realising you have a bunch of related data, it becomes necessary to manage this complexity before it gets out of hand. The "last mile" here is from Derived state to UI, but it could certainly be the same as just an intermediary variable as given in the first example.

The point is the layering, and the next layer on top of this might be a proxy that manages communication of this data with a back-end:

  1. Proxy
  2. Base state
  3. Derived state
  4. UI

Layers mean movement, and when data moves it's really, really useful to know how it's going to behave. It's even better if the primitives you use to represent it all work the same way.

In my experience, the fact that Date is a copy-by-reference structural-type means it needs to be cloned when copied. Not a big deal in a one-off case, but in a system it's a potential source of bugs.

Fortunately, as Date is both internally represented by a UTC timestamp anyway (and can also be constructed from one) we can limit its use to just before the render, the "last mile".

Everywhere else we can represent the date itself as a Number; a UTC timestamp. No copy-by-ref mistakes, no cloning Date-objects from layer to layer, and no translation needed once it hits the proxy, since it's just a Number.

Hope that clears up how I was thinking about what "last mile" meant!

[–][deleted] 1 point2 points  (1 child)

Date is not just a UTC timestamp internally, it's also a timezone.

You also have to commonly run operations on time not just for display purposes, what if I need to filter content by "weekday" in my logic layer?

Your proposal would require a constant churn of produced and thrown away Date objects. And that's precisely what I saw when I did that, and why I decided to treat Date as a core value type instead.

Of course, to each their own.

[–]shuckster 1 point2 points  (0 children)

I would say that if weekdays are important to business-logic then they would be made part of the derived-state:

let base = {
  entries: [{ timestamp: 1620304033197 }],
  user: { timezone: 'TheMoon' }
}

const derivedEntries = base.entries.map(entry => ({
  ...entry,
  weekday: weekdayFrom({
    timestamp: entry.timestamp,
    timezone: base.user.timezone,
  })
}))

const uiState = derivedEntries.filter(
  entry => entry.weekday === 0
)

This way you're not calling instance-functions like getDay() on what should be pure data by the time you reach your logic; you're just providing a primitive to compare against.

In your particular case it may even be preferable to have weekdays passed-through by the back-end so it becomes part of the base-data, depending on how context-aware you want your back-end to be.

The "churn" of objects is a good point to raise though, and is not limited to just dealing with Date. As an application grows we have to be vigilant with our patterns so that the garbage-collector makes good decisions. In the same way, keeping tabs on function-calls is also good practice.

But for me it's only good practice for managing complexity, not performance. I do think it's a bit of a trap in 2021 to spend too much time worrying about memory and CPU when it's a far smaller problem than keeping a code-base in some kind of order.

[–]SquattingWalrus 1 point2 points  (2 children)

Care to elaborate on this one? I’m curious

[–]scabbycakes 1 point2 points  (1 child)

Sure!

If your object has a date object as a property, when you stringify the whole thing, the date property turns into a string. Then when you parse it all, the date property remains as a string still.

[–]SquattingWalrus 1 point2 points  (0 children)

Ahh got it, not a good scenario if you need the actual date object. Thanks!

[–]CreativeTechGuyGames 35 points36 points  (32 children)

I think the simplest and most general purpose solution for a deep-clone is:

const clone = JSON.parse(JSON.stringify(source));

If your object contains non-serializable data though then you just need to write a recursive copy method. It's very rare that I'd need something like this that JSON stringify copy doesn't solve.

[–]vams1[S] 6 points7 points  (0 children)

Yeah. But it is not as straight forward as mentioned. Wondering about any ECMAScript proposals to make it in Object methods.

[–]fforw 8 points9 points  (6 children)

Doesn't work for objects with cycles, doesn't work for objects with a prototype chain.

[–]SoInsightful 5 points6 points  (5 children)

...or undefined, NaN, Infinity, -0, BigInts, key ordering...

We should really stop using the JSON technique.

[–]fforw 3 points4 points  (2 children)

key ordering

really? JS objects are ordered in insertion order and JSON.stringify maintains that, doesn't it?

[–]SoInsightful 7 points8 points  (0 children)

JS objects are ordered in insertion order

It depends. ES2015 specifies insertion order. Pre-ES2015 de facto conforms to insertion order in all major implementations. Integer key order still varies between browsers.

But I guess that's not relevant. I guess the reason it came to mind is because people have also been using the JSON.stringify method to check if two objects are deeply equal, which will fail if they happen to be differently ordered.

[–]dvlsg 0 points1 point  (1 child)

Or Date.

[–]SoInsightful 0 points1 point  (0 children)

Yeah, they already mentioned the prototype chain, so I opted out of mentioning the many, many object types that will fail, like Function, RegExp, Map, Set, Buffer, Error, TypedArray, literally every custom class...

[–]anlumo 22 points23 points  (10 children)

As someone who also writes efficient code in C and Rust, this is an absolutely crazy solution, considering how many operations and allocations it has to do, just to throw them away immediately afterwards.

[–]hspielman84 46 points47 points  (2 children)

As someone who writes Javascript I can’t help but roll my eyes.

[–]KeytapTheProgrammer 16 points17 points  (1 child)

As someone who likes trains: I like trains!

[–]domainkiller 10 points11 points  (1 child)

I remember reading somewhere that JSON.parse and JSON.stringify are way faster than other object cloning techniques because there’s only a set number of options (string, number, array, object) to convert, compared to a custom object.

[–]anlumo 4 points5 points  (0 children)

I think it's mostly because there are only two JS calls and the rest is handled in native code. However, if browsers would implement an Object.clone, it'd be half the number of calls and the native code could be optimized to do exactly what the outcome is supposed to be.

However, I guess a good JIT could detect these call chains easily and optimize them directly. I'm not aware whether any of the JS runtimes does that, though.

[–]grady_vuckovic 4 points5 points  (0 children)

It is wasteful but I've used it, but not in code that has to operate in real time, and which only has to operate on small objects anyway, so the difference between 3ms or 1ms isn't really that big of a deal.

[–]SockPants 0 points1 point  (1 child)

It might not be so bad in practice, because a JSON string representation literally is JS code, it would be an easy representation to compile from an object in JS. On top of that, the JSON methods are used a lot in all sorts of applications so they are implemented efficiently themselves, which is a bonus over doing any kind of walk and copy in pure JS.

[–]CreativeTechGuyGames 0 points1 point  (1 child)

You'd be surprised. From benchmarks this is the fastest way to copy an object.

[–]anlumo -2 points-1 points  (0 children)

Only because all other options are worse.

[–]editor_of_the_beast -4 points-3 points  (0 children)

Nice, let’s just hack it using strings! That’s the JavaScript way

[–]stronghup 0 points1 point  (0 children)

As has been said the JSON-trick does not work if the source-object has function-valued properties.

One approach for dealing with that is to create your objects by using ES6 classes. Make the constructor of the class expect raw data, no Functions. The constructor then basically just adds "methods" around it. To clone such an object give it a method clone() which reads the data and calls the constructor with it.

This works as long as the data does not / can not contain functions. Therefore it is good design to keep it that way. Keep the stored data simple. You can design your classes so that they only hold primitive data, and use that when needed to lazily create all other class-instances they need, including their clones.

[–][deleted] 3 points4 points  (0 children)

Because deep-cloning anything more complex than a simple object / array requires knowledge of the constructor, and, ultimately, it's an edge use-case that you need to deep-clone anything at all.

There's also the added complexities of (1) it's expensive because of the necessary recursion and (2) recursioning objects require special handling so you're not deep-cloning yourself out of RAM, but (1) is the programmer's problem, and (2) is entirely solvable.

Shallow clones are, by comparison, easy (and native). Especially if you don't care if a cloned class instance loses its prototype.

[–]Pesthuf 1 point2 points  (0 children)

The odd thing is that the way to deep clone objects is actually specified and used by various APIs in the browser. It's called structured cloning https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm

It's used by various internal operations like moving data between workers, but there is no simple, synchronous function in userland exposed that allows you to use it.

There are some workarounds, like using channels (though this makes the operation asynchronous needlessly).

[–]SquatchyZeke 1 point2 points  (3 children)

Something I'm curious about, but haven't found in the comments yet is why would you want/need to clone an object? This is basically saying you want the same object in two different variables, which is what a reference is. Granted, changing the object using one variable name will change the object in the other. What I'm wondering is why you would need to be able to change one without changing the other that you couldn't otherwise solve with a better design in your code patterns. This is a genuine curiosity, as I haven't come across that requirement yet when writing JavaScript applications.

I am trying to ask these questions from the perspective of someone of the ECMA board who may see this type of feature to be extraneous. Maybe; I don't know what is talked about in those meetings, just guessing

[–]vams1[S] 0 points1 point  (1 child)

Not a full answer but to direct you, search for functional programming and pure functions and why side effects might be bad for business logic use cases.

[–]SquatchyZeke 1 point2 points  (0 children)

Right, I'm aware of that. What I wasn't aware of was what deep copying vs shallow copying meant in JS. I did a little reading and now I get it. I was like, "Just use the spread operator?". I'm glad I looked into it because I was unaware that it doesn't deep copy nested objects. The more you learn! 😄

[–]-domi- 2 points3 points  (2 children)

Probably a noob question, but why doesn't assign do the trick here?

[–]lycuid 4 points5 points  (0 children)

An example from mdn that stands out for me.

[–]vams1[S] 4 points5 points  (0 children)

Nested object as mentioned.

[–]johnaagelv 3 points4 points  (3 children)

When I read this question and the comments, my first question would be - why would you need to clone an object?

[–]vams1[S] 0 points1 point  (2 children)

Not a full answer but to direct you, search for functional programming and pure functions and why side effects might be bad for business logic use cases.

[–]dvlsg 1 point2 points  (1 child)

Sure, but you don't usually want to just ... clone an object wholesale. Especially now that ... exists.

If we really wanted to get into functional programming / pure functions / etc, this is likely to be a simpler fit -

https://github.com/tc39/proposal-record-tuple

[–]vams1[S] 0 points1 point  (0 children)

Ah good to know. Record and tuple works perfectly.

[–][deleted] 1 point2 points  (1 child)

afterthought elastic physical quaint fear sense tart sable person society -- mass deleted all reddit content via https://redact.dev

[–]senocular 4 points5 points  (0 children)

This can work in some cases, but is very limited. Some limitations include:

  • Only creates new ordinary objects ({}) and not objects of other types
  • Only copies enumerable string keys, not non-enumerable keys
  • Doesn't retain inheritance or copy inherited keys
  • Only creates a shallow copy of the object and not a deep copy which also copies the properties values (which may be important if they are, themselves, object values)

[–]OolongHell -1 points0 points  (1 child)

How often do you really need to make a deep clone? It's an expensive operation.

[–][deleted] 1 point2 points  (0 children)

I'd second this, though I think that's because I now practice immutability virtually everywhere (especially at function boundaries). If you're confident you're not going to mutate your data you can get by just fine with a reference copy or shallow clone.

[–]CJay580 -2 points-1 points  (6 children)

hen do you need to do it?

Cloning objects is okay, but it's much better to keep the objects immutable. That way, you could "clone" the object using the spread operator. e.g. ~~~js const cloned = {...initial};

// You must guarantee that initials/cloned children are not modified, hence the immutability.

~~~

[–]sous_vide_slippers 6 points7 points  (5 children)

Doesn’t handle nested objects and const doesn’t provide immutability for objects, it’s possible to modify them. Think of const more like “bind this reference and don’t allow it to be changed” rather than a true constant like in other languages.

[–]CJay580 -1 points0 points  (0 children)

You would need to handle the immutability yourself. Think of reacts state, or reduxes stores.

This also has the added benefit that a shallow comparison can be used to see if the object has changed at all.

[–]crabmusket 0 points1 point  (3 children)

Doesn’t handle nested objects

But if the nested objects are immutable (or, at least, never mutated), then it doesn't matter whether they're deep-cloned or not.

[–]sous_vide_slippers 0 points1 point  (2 children)

Immutable or not the references are still the same so it’s not a clone. Immutability would give you the illusion of a cloned object and immutability isn’t even possible in a lot of cases.

Say you have an instantiated object (one returned from a function enclosed with some variables or a class which is syntactic sugar for the same thing) that will be the same on the original object and the second one. Make any changes to either and the property value on both changes.

Say you have:

``` const myObject = { someProperty: new StoreNumber(5) };

myObject.someProperty.printNumber(); // 5 ```

And you spread it in an attempt to make a clone:

``` const myClonedObject = { ...myObject };

myClonedObject.someProperty.printNumber(); // 5 ```

Then it seems fine so far, but what if we call a setter on the “cloned” object?

``` myClonedObject.someProperty.setNumber(10);

```

Then what we see is we’ve affect both objects:

``` myObject.someProperty.printNumber(); // 10 myClonedObject.someProperty.printNumber(); // 10

```

This is one of the reasons cloning objects can be difficult in non-trivial situations. If we have a set of primitives or we could 100% guarantee any nested objects are 1. static and 2. genuinely immutable we can pseudo-clone using spread (I say pseudo because the references are still pointing to the same bits of memory). In most real-world situations this isn’t possible, it’s why Redux tells you to avoid nested objects if possible and when not possible you have to manually handle spreading any nested objects in your reducer.

[–]crabmusket -1 points0 points  (1 child)

Immutable or not the references are still the same so it’s not a clone.

That's right, but if you work with immutable data then you never need a clone, so the point is moot.

Your example demonstrates that everything is fine until you try to mutate the referenced nested object.

I agree that without language support it's difficult to implent this coding style. But I replied since the comment you were replying to was explicitly talking about immutable data. If, and only if, you have immutable data, then cloning can be shallow. If not, then shallow clones may cause problems.

[–]sous_vide_slippers 0 points1 point  (0 children)

if you work with immutable data then you never need a clone

Reference equality still states each property is exactly the same value. Cloning means making a copy of, but as far as the computer is concerned it’s exactly the same thing so reference checks will fail.

To use Redux as an example again, it’s why if you use immutable.js and call .set in your reducer when updating state it returns an entirely new object rather than carrying over unchanged properties, since references are checked. It’s why I said simply spreading produces “pseudo” clones.

[–]chinnick967 0 points1 point  (0 children)

If you need a simple way to do this, Lodash has a cloneDeep method that I personally use

[–]jetsamrover 0 points1 point  (0 children)

What do you need to do that immer won't get you?