Hacker News new | past | comments | ask | show | jobs | submit login
Deep cloning objects in JavaScript (builder.io)
201 points by kiyanwang 4 months ago | hide | past | favorite | 170 comments



If you’re reaching for structuredClone, what you really want is native immutable Record and Tuple syntax, and the companion “deep path properties” syntax which allows for efficient and ergonomic immutable updates:

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

- https://github.com/tc39/proposal-deep-path-properties-for-re...


Also, if one found their way to immutable Records and Tuples, maybe it's also time to start exploring Clojure/ClojureScript.


I dream of a world where ClojureScript became the common front end language of the web instead of JS and what that would have done for the language and ecosystem.


JS: deep copy so awesome!!

CLJS: ¯\_(ツ)_/¯


Maybe. But I think there’s room to bring more ideas from Lisps into a language at least partly inspired by Lisps.


There are other reasons for cloning objects apart from immutable updates. Might also be you just want to pass around an object reference and make sure it is not mutated later, so you use a clone.

And structuredClone was standardized to serialize objects passed between contexts.

So I don't see what's wrong with teaching people about this as the native way to clone serializable objects.

Record/Tuples are different types and I don't think that "what you really want" is a future proposal when you want a built-in now.


You’re mostly right, I should have said “what you _probably_ want” instead.

Misuse of deepClone & co is just a very common antipattern.


Yeah it is really easy to fall into a misuse of expensive object memoization with deep equality checks if you try to "do it right" in React for example, and work without an external state management library.

Or when working with libraries that demand this.

In that spirit, I find MobX pleasant to work with as an alternative.

Although it remains needed to grok reference equality checks in reactivity, there's no way around that I guess.

Fundamentally, when passing messages with structured payloads I'd consider it good style for the payload to be serializable and free from references and mutable state.


While I am extremely excited that these may someday make it into the language, I actually would be fine with `.hashCode` and `.equals` on the Object prototype.

structuredClone, if I'm not mistaken also allows cloning functions/Maps/Sets, so while there may be some overlap, I'm not sure having Records and Tuples solves the same problems. Or as nerdy engineers are so prone to say, they're "orthogonal".


> if I'm not mistaken also allows cloning functions/Maps/Sets

Map and Set yes, but not functions. Makes sense because there is no reasonable way to serialize functions in JS.

https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers...

edit: I have no idea what actually happens to cloned maps with function values or keys though...


I know Reflect[0] doesn't get alot of attention however I always thought it would be a big improvement if `Reflect.set`, `Reflect.get` and `Reflect.deleteProperty` gained support to deal with nested properties. It would effective give developers built-in JSON Path support[1] (like you see in MySQL, Postgres or SQLite databases).

As an unrelated aside, recursive proxies would also be extremely useful

[0]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

[1]: https://goessner.net/articles/JsonPath/


Instead of the "deep path properties" syntax, you might want to take the opportunity to learn about functional lenses with monocle-ts

https://gcanti.github.io/monocle-ts/


This is one of the things I hate about the JS ecosystem.

"instead of learning x, use y library" Sometimes that advise makes sense, yes. But it's constantly used in a dogmatic manner.


The hash symbol looks really out of place in js.


it may do, but it has prior use already: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...


I'm not a fan of it there either. JavaScript, since its earliest days, had `private` as a reserved word. Typescript makes use of it. It conveys perfectly what the Class member's visibility should be. Using `#` might be easier on parsers, or may fix some sort of compatibility issue (I haven't read the reasoning) but it's silly.


There's a lot of reasoning for it, mostly summarized by "this.abc and this.#abc are not the same fields".

The goal of #-private properties in JS is to have total runtime isolation, so that there's no way at all to access it from the outside. If you're fine with a compile-time error, use TypeScript's private keyword instead (JS does not have a compiler).

But why can't `otherObj.abc = 3` throw an error if abc is declared as private? Because it would incur a runtime cost on all property accesses to check whether it's public or not, even if private properties aren't used anywhere within the class. Not a sacrifice anyone would be willing to make.

So, the solution is to make sure that private property accesses can be distinguished from public property accesses. You could have had something like `private.abc` instead of `this.#abc`. But I don't think that's better, either.

Honestly, you'll get used to # if you use it for a while.


This is similar to the recent post about private members in Ruby. It's a sytaxical check that the receiver is literally 'this'. Which, like you say, can be checked at parse time without any lookups. Even identity(this).privateVar doesn't pass in Ruby due to not being sytaxical this.


TIL, thanks. But uh, I expected `JSON.stringify` to throw errors when met with unencodable objects, like Python does. But it silently corrupts the values. A typical JavaScript thing.

I also think the automatic conversion of a `Date` to a string is a bad thing, which again is prohibited in Python.


It doesn't "corrupt" them; it converts them to JSON strings. It's in the name. Expecting `JSON.stringify` to throw in those situations would be like expecting `str` in Python to throw.


It does not convert them to JSON strings, as stated in the original article. That's why I said it corrupted the values.

  > JSON.stringify({a: ()=>{}})
  {}
  > JSON.stringify({a: /b/})
  {"a":{}}


The first seems ok to me. Kotlin and Java does the same - ignore methods when serializing into json


You can use a replacer function to special handle stringifying functions if you needed to do that.


What irks me is its default behavior. A function should do safe operations by default. Compared to `JSON.stringify()`, 'structuredClone()` throws an error when it encounters values that cannot be cloned, which is a much saner approach. Probably because they learned from the mistake.


I understand your point and I can’t deny that Javascript continues to introduce weird, silent failures and quirks even today when everything is a bit more thought out than “the bad old days”.

But I think in the case of JSON.stringify it’s more about use case. 99% of the time, users of this method are taking some data and serialising it to a JSON compliant form to send to the server. JSON doesn’t support functions, or complex objects like a Date, so I tend to think it’s a reasonable default that functions disappear and Date’s are converted to an ISO standard. To insist that every single user runs a preparation step that strips out unserialisability data and chooses how to handle Date objects sounds laborious, error prone, and ripe for another npm dependency everyone suddenly normalises for every project.

Maybe a “strict mode” of some sort where you could have it throw on anything for cases where you need to guarantee everything is being sent?

OTOH, I have to concede that while this method has silent failures, they then implemented JSON.parse to throw at the slightest issue. So I have to admit there’s consistency even within the API.


I guess there's some good reason nobody ever did it, but what about throwing an UnhandledType kind of error and letting the catch() decide how to deal with the object in question?


On top of the compatibility issue mentioned in sibling comment, the existing behavior also matches the principle of keeping simple things simple without making complicated things impossible. If you want stringify with a check for unhandled elements, that's easily specified in a replacer (that does not really replace).

You might prefer some well established standard implementation over ad hoc roll your own, but that's a discussion about npm culture, not about stringify.


Browser care about compatibility a lot, the ship to change this behavior has shipped a long time ago.


>and ripe for another npm dependency everyone suddenly normalises for every project

Now now, it's only 1M weekly downloads: https://www.npmjs.com/package/superjson


Interesting, I’ve never seen this, thanks!

The problem I see with this is that whatever you’re sending this to must have knowledge of the meta information superjson produces, so at that point you’re investing in it as a wrapper library. The fact you can extend the types it serialises also complicates things and means the receiver needs further implementation specific knowledge.

I think in my original comment, I was imagining a world where JSON.serialize threw errors on unknown types and we needed a wrapper just get basic JSON out of it.


Meh JSON.stringify works as advertised


`str` is the equivalent of `toString`, not of `JSON.stringify`. Failure of a serializer-deserializer to roundtrip properly is corruption. The poorly chosen name (which is usually called "dump" or "serialize" in other languages) does not give license for silent corruption.


"stringify" sound more like toString than serialize to me.


I think that is the issue.


I agree with everyone here: it’s named correctly, but other languages did it… differently. (Stopping short of “better”, but only just.)


That is the solution. It allows JSON.serialize to exhibit the non-corrupting behaviour, giving a reasonable indication to the developer which is which by the name alone.

The issue, if there is one, is that nobody ever got around to implementing JSON.serialize.


If a JSON.serialize function did exist, would it do the same thing as JSON.stringify, just with catchable exceptions when one tries to serialize something that can't be (e.g. functions, date objects)?


It depends, it does 'corrupt' Sets and Maps. But it can throw errors on other values. For example it will throw when a field has a value of undefined, or when there are circular references.


for those considering Immer, check out Mutative or Limu instead (both much faster)

https://github.com/unadlib/mutative

https://github.com/tnfe/limu


I think there's a typo in the cloneDeep example[1]. cloneDeep is imported but never used, and structuredClone is used instead.

[1]: https://www.builder.io/blog/structured-clone#why-not-code-cl...


Agreed


As a JS/TS dev since the early node says I still can't believe how long we used the json stringify method for.

Not because it worked well, but because it was _good enough_. I feel like that in itself is an important lesson about our industry and probably the world.


It reminds me of using the STUFF() + FOR XML PATH trick in T-SQL (mssql) for so many years to aggregate row values into a comma separated string. Now you just call STRING_AGG()


For data to rendering on UI, JSON.parse(JSON.stringify()) is enough. For other operations, clone an object with such complex structure is not a good idea, you may change your way of coding.


Agreed, generally. But it may be nice for saving complicated game states, or sending complicated objects to a Worker without having to rebuild them from JSON.


Wow, that's nostalgic. I remember implementing and writing about deepCopy() in JavaScript over 14 (!) years ago[1]. If you read the OP article and were left wondering how such an algorithm actually works, that old article explains it in some detail. The basic idea is still the same; only the handling of edge cases introduced by newer language features is different.

That implementation eventual became obsolete (although it still runs on basic object types) because of newer features being added to the JavaScript language, and by then other implementations were available. It's great to hear a de facto standard is emerging, even if hasn't made it into ECMAScript standard yet[2]. It sounds like the same edge cases are still causing problems. I think if a language is to ever offer really great support for deep copy it must be designed into the language as a first-class feature from the get go.

[1]: https://www.oranlooney.com/post/deep-copy-javascript/

[2]: https://es.discourse.group/t/structuredclone-as-ecmascript-s...


Well it sounds like it’s a web standard and clones a heck ton of browser objects that ECMAScript wouldn’t be able to require it to clone. But yes, I suppose requiring it to exist and work on non-host objects in ECMAScript would be ideal!


I wasn't aware of this, I would like to give a shout out to superjson (https://github.com/blitz-js/superjson) which I had been using to solve this problem. I will look at this solution for next time.


Actually what you linked looks superior in be case of: client + server. If I need to send objects over the wire that have Maps and other properties, that appears to be a better method.


> structuredClone({ a: () => {} })

> VM187:1 Uncaught DOMException: Failed to execute 'structuredClone' on 'Window': () => {} could not be cloned.

Ah, yes. I actually asked this question on StackOverflow 5 years ago [0], and the reason it can't be cloned isn't the worst reason ever... but it basically breaks cloning any non-trivial object. Well, I suppose I'll check back in another 10 years when the next ecmascript proposal around cloning lands...

[0]: https://stackoverflow.com/questions/51939812/how-to-clone-an...


It is mentioned in the exception list of structuredClone.

https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers...


I disagree that this is trivial.

You have a function, and that function could have local bindings to arbitrary local variables.


I didn't say that the algorithm was trivial, just that the object was. `{ foo: () => {} }` is a fairly trivial object, all things considered.


It's not an object. JSON wouldn't cover that. It's code. With code complexity. That's non trivial & you seem to double down on an extensive ask far beyond .odt people's & what the typical need is, in a way that belittles the effort. I don't like how short you are, how you phrase this as some huge weakness. When in fact JSON.stringify/parse - what everyone uses today - have the same object-but-not-code limitations.


Functions are first-class objects though…? What’s json got to do with cloning objects except its prevalence as a limited workaround?


It's not just an object though. It's an object that has potentially has implicit bindings to something in the rest of the runtime. Bindings that (to my lament) aren't visible in the first clas-object; secret hidden internal state, tying it to the rest of the runtime.

JSON defines what is possible to do that doesn't involve arbitrary bindings to the rest of the runtime. Short of serializing the world, JSON is the best we can do.

It would be possible to try to serialize, and maybe a function is simple enough to serialize. Do we have existing examples of this? There's hundreds of different serialization libraries. For some reason everyone in this thread is super ignoring what seems like a basic widespread limitation, is being condescending to structuredClone, for not doing what no one else does either. Because there's a very good reason: because here be dragons. I don't get why everyone is so combative & aggressive over what seems so clear.


Why are you talking about JSON? This was a discussion about structuredClone.

> I don't like how short you are, how you phrase this as some huge weakness.

I think you may be reading into my comments more than what is actually there. I think it's a reasonable enough compromise, and I'm happy to have structuredClone.


It's been a little while since I wrote JavaScript. Aren't Function objects immutable? Would it not satisfy the desired behavior of clone to reference the same function (not copy it) in the new object?


Even if functions were immutable as objects, they can reference anything within their containing scope, including arbitrary dynamic variables in outer scopes and even values from other modules (whose bindings are “live”). They may also have their “this” bound to arbitrary objects from any scope at all.


Function objects are mutable in the sense that you can define new properties for them or assign new values to (writable) existing properties. This is sometimes used to simulate static variables:

  function f() {
    f.count ??= 1;
    console.log(f.count++);
  }
  f(); // Outputs: 1
  f(); // Outputs: 2
  f(); // Outputs: 3



Why go with less recognizable "structured" in the name instead of "deep"?


I just did a deep dive into this (pun intended). The name appears to date back well over a decade (probably longer) and has roots in lower-level browser APIs like the implementation of `postMessage`.

> Structured cloning algorithm defines the semantics of copying a well-defined subset of ECMAScript objects between Code Realms. This algorithm is extensible by host enviroment to support cloning of host objects.

https://github.com/dslomov/ecmascript-structured-clone

Eventually, in 2015, it was suggested to expose the `structuredClone` algorithm as an API:

> Has anyone ever proposed exposing the structured clone algorithm directly as an API? Katelyn Gadd was musing about fast deep copy in JS on Twitter, and I proposed a hack to use postMessage to do so[1], which works but it's a little roundabout. Since structured clone is a primitive that the web platform is built on it seems like a primitive that ought to be exposed. I know this exists in other languages (Python has copy.deepcopy[2]) and there's an npm "deepcopy" module[3] with lots of downloads so this is clearly something people use.

https://lists.w3.org/Archives/Public/public-webapps/2015AprJ...


Thank you, have you seen whether there was any consideration of the obvious naming flaw? Don't see anything in these links


I was trying to find something about that, but wasn't able to. I would be very curious if someone else can find meeting minutes or mailing list posts where they discuss it.


What is the flaw exactly? Or you mean that "deep" is just a better/more well-known name?


So that you know it is internally using structured serialization, and all the caveats of that?

To me, deep clone implies that I will get back an exact copy of what I put in. structuredClone does not make such guarantees.


The naming fails here as well - "clone" imlies it's an identical copy, so it contradicts whatever deep knowledge of structured serialization you have


It does, but in fairness we are talking about the second hardest problem in computer science. I, for one, prefer it to deepCloneKindOfButNotReallyWatchOutForTheCasesThatMightSupriseYou.

"Structured clone" is enough to give you pause and question "what could structured mean?" (just as you did), at which point you will read the docs and find out. It succeeds in communicating what it needs to.


Oh, no, I didn't really question "what could structured mean", nor do the docs explain what "structured" means (https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers...), so what exactly does it succeed in communicating? One thing it succeeds in hiding is the first thing mentioned in the docs - "method creates a deep clone" (funnily enough, linking to an article more appropriately called "Deep copy")

What gave me a pause (and would be part of "yet another bump on the road to intuitive use") is the disconnect between the awareness of the better naming and the choice of a worse one (so, in a sense, this hardest problem has already been solved by other computer scientists!)


> nor do the docs explain what "structured" means (https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers...)

The actual docs do. This second-party attempt to recreate the docs in their own way that you point to has fallen short, I agree. Granted, Mozilla does a better job than some of the other second-party recreations, like Deno's. There is little question that the Javascript ecosystem is a bit of a disaster.

That said, I clearly see the caveats right there up front and centre, so even still it has successfully managed to provide you with what the function name wants to bring to your attention.


I cannot believe lodash cloneDeep (just the one function) is 17.4kb minified!


A lot of Lodash functions are implemented as combinations of other Lodash functions, so importing a single function actually imports half of Lodash under the hood:

https://github.com/lodash/lodash/blob/main/src/.internal/bas...


Would it be shorter if it didn’t, though?


Glimpsing at the source code - yes, definitely.

For example, the code imports "copyArray", a 9LoC function that does exactly the same as the built-in Array.slice() would do.

Same goes for other imported helpers like "arrayEach", which could be replaced by the built-in Array.every().

It's basically a bunch of unnecessary polyfills for built-ins.


Unnecessary _now_ perhaps, but not unnecessary when lodash was written. In 2009, when underscore.js came out (from which lodash was a fork) Array.every() was not something you could rely on. Chrome just came out a year earlier (and didn't support Array.every until version 4) and half the world was on IE 6-7-8


The expectation is that you import the whole library and not just one function. At which point, lodash is smaller than if each function was implemented separately.


I actively fight for removal of lodash in every frontend codebase I encounter. The cost is just too much. Often I run into developers who tell me there's some sort of webpack plugin to treeshake all the `import _ from 'lodash';` nonsense. The truth is, when we run a bundle analyzer, it's still one of the biggest bloaters. Webpack is also becoming less common in newer codebases. The point is, while it provides a ton of convenience methods, there is a cost.


Not cloning instances of classes properly is a surprise. Is there a particular reason for that?


structuredClone is a standard library implementation of the structured clone algorithm, which was originally mainly designed for passing data between realms (eg between threads). Prototype chains are not preserved across realms, so it makes sense that the algorithm wouldn’t either. The accommodation for native/host types is possible because they’re implemented in each realm by the runtime, but user-defined classes don’t have that benefit.


On the one hand, you’d think the copy would just have a prototype that points to the same as the original’s prototype, and that would make this all work out. I’m surprised that’s not what happens since in 99% of cases it would be fine.

On the other hand, when you consider #privateMembers, you realize cloning a class in JS is basically impossible (and adding them to the language was probably a horrible mistake).


Classes have way more complex state that could be unpredictable


Looks like Webkit doesn't support it in workers. Does anyone know why that would be difficult?


well it was only adopted in 2022 across browsers. Where did you find that it's not supported in webworkers? This makes me think it's functional

https://bugs.webkit.org/show_bug.cgi?id=228331


A screenshot of caniuse or similar is in the linked article.


The screenshot was from MDN [1] but today the support table is all green; no caveats for workers.

[1] https://developer.mozilla.org/en-US/docs/Web/API/structuredC...


I've never had the need to clone anything in a real JS code-base, personally.

What's the use-case?


It is very frequently needed when you're working with a component framework like React or Vue. Typically leaf components shouldn't mutate properties directly but rather emit a changed version of the original data they receive.

But it's not necessarily related to frameworks; if you're working with complex enough data structures and you're following a functional approach, you'll need to do similar things sooner than later.


I use React.

Why does that require deep clones?

I simply do:

    return {
      ...current
      foo: "bar"
    };


I'm not sure if you're asking why deep copies are useful or something else.

Maybe you're handing over your data to a library developed by someone else and you want to make sure the library cannot mutate your original data, so you opt to pass a deep copy instead. Or maybe you are the author of said library and you want to make sure you preserve the original data so you copy it on input rather than on output.

There are many situations where deep-copying is useful but I agree that you should use the simplest pattern that works for your use-case.


if "current" is a deep object here and contains other objects/arrays, you risk that wherever you are sending this shallow copy will mutate those deeper values and potentially mess things up for the code where "current" came from.

Maybe it's not a situation that comes up often, but it would be fairly hard to debug and guarding yourself against mysterious problems in advance is always neat.


In practice, I think it's easier to use linting and type-systems to prevent other code from mutating you stuff than defensive copying.


You could make the same argument backwards though - many people may find it easier to do deep copies rather than throwing extra software they might not necessarily be familiar with.


When the caller passes you a deep structure and you want to ensure they don't mutate it afterwards. But I agree, it's seldom needed in application code.


I came across a bug recently where a data structure from Redux, which is immutable, was passed to a GraphQL library that would modify the data it was passed. So we had to make a deep clone.


Every time you get an object from somewhere, which needs to be preserved and modified at the same time.


Just make lodash part of the ECMAScript standard? It's been honed for 11 years and solves most of the gaps for this sort of utility function.


Wait, what? When did this happen?


Apparently, 1-2 years ago. I was also surprised.


Unpopular opinion:

I have never needed to "deep clone" an object in JavaScript. Any time I thought I needed to do that, it was actually a code smell for a different underlying problem.

I was glad to see the end of the "immutable everything" dogmatic obsession wrought by Redux. The code smell in that case was usually related to passing entire objects as props.

Can anyone describe a compelling use case for "deep cloning" objects in JavaScript (keep in mind _everything_ is an object in JavaScript) that isn't dancing around some hidden complexity? IMO, if it can't be (de)serialized via JSON.stringify and JSON.parse, then you should consider why you're trying to serialize it in the first place, and why you're scared to pass around the reference to the object. (That said, structuredClone looks like a better alternative to the JSON method - but note it's still not cloning functions, so you should still be careful where/when you use it.)


The most common reason I've had to do it is to log objects to the console.

Chrome dev tools logs a pointer to the object, so if the object is mutated after logging, but before you view/expand it (especially for nested structures), it can be very confusing! JSON.parse(JSON.stringify()) usually is good enough for this :)

But yes, generally speaking I think a deep clone is a sign of a smell elsewhere. That doesn't mean it never has a place in any code, but just that if I find myself reaching for it, I usually see if there's a mistake being made elsewhere.


Isn't that desired? You want to see what the object looks at at the time of logging. If you need to see it after some additional code has run, you log it again.


The problem is that if you log one object multiple times, they all point to the same object.

So if between two logs it was modified, it reflects the new status on the log before as well.

I think you may have been confused and got how it is, vs how it ought to be the wrong way round.


to be honest, my experience with logging objects is so simple, i've never needed to try this live look into the object. my expectation was the log is a snap shot in time, not a live look at the object. an interactive view into the object does sound interesting rather than constant log entries for each step. which is how I would have tried to do something the first time only to be confused by it. TIL.


What the parent posts are trying to hint to you is that the default logging behaviour of the console is the live-look model, which is almost never what you wanted with logging. (Though it does have other uses.) To avoid the live-look, you can deep clone, or just stringify.


That's just one of the reasons I always say...

https://twitter.com/justinvincent/status/1714866433426067573


Wow, I never knew this.


You have it opposite… if you change the object after logging, it will retroactively change the logged version.


> You want to see what the object looks at at the time of logging

Yes. That's why you need to use structuredClone. If you don't, and you mutate the object, then you'll see the new values when you expand the object.


> Can anyone describe a compelling use case for "deep cloning" objects in JavaScript

Sure. A library accepts an object as function argument (for example ‘params’) and then uses it inside. Modifying users object in-place would be a bad idea.


Another one is exporting an internally used object from the library, you don't want user to be able to modify it.


The smell here is in the library, is the commenter's point. The solution would be to search for a better written library.


Another issue is the opposite situation, where the library reads from the object multiple times, expecting the value to be stable, but the caller might change the value in between, either from the properties using getters or from the library being async. The library has to make a full deep copy to ensure that it's completely stable regardless of the caller's actions. Just freezing the input object would be confusing to the caller and still vulnerable to property getters.


That’s even worse! I would try to avoid a library with a function that fragile.


Sure! Use a library that deep-clones the object first, leaving the original parameter unchanged! And now it's slower than the original library you're criticising.


Or use a library that doesn’t modify the input…


By deep-copying it, right? Sometimes it is needed.


If you know what modifications you need to make (hopefully the library author does), you only need to clone the relevant parts of the object (e.g. with spreading). Deep cloning still isn't usually necessary unless you're doing wierd stuff.


The Date object would like to have a chat with you ;)


I know you were mostly joking, but for cloning dates you can "just" use the constructor (https://stackoverflow.com/a/69601256).


…the whole thread you’re replying to is about it not actually being necessary. To participate in this conversation you either accept that premise or disagree with it, passing it as a given is not really an option for the argument.


There are plenty of reasons to deepclone. That being said I do share some agreement. JavaScript imo is best when you limit complex structures and stick with primitives, ideally just one layer deep, but there are situations when it can’t be helped, e.g, dealing with api calls.


Yep. But API calls traverse the network, and assuming that's via JSON, you're already losing functionality like Date parsing, so the litmus test of JSON.stringify/parse seems appropriate to determine if you're dealing with a true "data object." If you run into issues with things like Dates, then it's a smell indicating a potential point of divergence in different codepaths that handle the network call differently.


I don’t disagree, but just responding to your comments first paragraph.


Immutable structures are beautiful when I’ve got an editor for geographic data and want a high performance undo/redo stack with basically no effort. It’s also considerably cheaper to identify when groups of polygons have changed by object identity rather than deep equality. Did this property change? Did properties in general change? Did this polygon change? Did this collection of polygons change? All using === equality checks without any manual plumbing. About ten years into doing it this way and I’ve got one regret: starting with ImmutableJS instead of Immer.


If something is immutable then you never need to clone it. Just copy the reference.

If an object is immutable and you want to copy/mutate one item it then you can surgically clone it in O(how deeply nested the item to change is). Object spread being the simple case.

JS makes this messy to do though! Lots of spreads and array maps or you pull in a library like Immer and now have “coloured” objects. Immutability in JS is not idiomatic so for that reason I try to avoid unless I really need to.


> Can anyone describe a compelling use case for "deep cloning" objects in JavaScript

Hierarchical data structures, in mindmaps, menu structures, block based notes apps, etc.


I agree. I had this same problem with a tree UI model. Each node had links to both the children and the parent. Mutating any node in the tree, changes the reference of that node. Mutating the linkage to the updated node cascades to every other node in the tree.


An edit dialog where you want to be able to cancel editing without leaving changes on the original object.


Undo. I use it in UI Drafter for handling Undo.

For example, I create a revision of a store (a deep clone) and apply the undo frames one-by-one.

Each undo frame has the setter name and its arguments. IOW, the frames contain what changed, such as:

    [
      [BASE.setCard, 'some-id', CF.title, 'The New Title'],
      …more frames
    ]

Here's a post with more details

https://blog.uidrafter.com/architecture-of-a-desktop-alike-s...


Yep, I do the same.


I have an application with a big data structure built as a tree of objects. With deep cloning I can: - Provide undo/redo. I just clone the whole tree. - Load/Save from/to the database part of the tree. I load it in a separate object and then deep clone in place. - Do mass modifications to a part of the tree. A clone a copy, apply modifications and the clone back in place.

I could do some of the changes in place, but to optimize Vue reactivity I work on copies.

I use prototypes so only lodash works for me.


Pedantically, while _everything_ is an object is true for Python it’s not for JavaScript which has distinct primitive types for strings, numbers, booleans, symbols, and undefined.


And null. I know that typeof(null) says 'object', but it's lying. When I say "it's lying", I mean that according to the spec, null is of type null. Also it has absolutely nothing in common with objects except for the output of typeof.

Also these days there's BigInt.


JS is amazing.

As in, it’s amazing that anything actually works given the internal inconsistencies in its core.

(Disclaimer: I code ts/js for a living.)


I am building a card game engine in typescript. A simple AI based on monte carlo tree search requires me to copy the gamestate a lot. And it's always a list of cards that i'm modeling so I really need to deepclone arrays.


Can you not serialize this state to/from JSON? I'd encourage trying to reduce the state to be JSON-serializable, and then re-initialize any "classes" after deserializarion with a .fromJSON() method (the "factory method" pattern) - in fact you can even call such .toJSON() "replacer" [0] and .fromJSON() "reviver" [1] functions in a callback passed to JSON.stringify and JSON.parse.

[0] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

[1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...


How is this better than deepcopying?

Deserializing requires paying all the same costs as deepcopying, both in terms of compute time and in terms of memory usage, but it also incurs the extra costs of parsing.

I'm not saying that there wouldn't be benefits to having a serializer for their gamestate (because there would surely be benefits), but I just cannot understand this as an alternative for the problem they described.


I haven't really gotten to the point where anything works but doing the serialize de/serialize combo is possible. The issue is, that I am thinking about doing this not a few hundred times but more like several million times (preferably per minute), so i'll likely have to implement several competing versions and benchmark them.

The full idea is implement a deckbuilding game like magic and use genetic algorithms and montecarlo simulation to balance the card pool. But that requires millions of games with millions of simulations to work. So i'll see if it's even possible.


If you really need maximum performance then you probably don't want to deal with JS objects at all. (But you should benchmark it first... you might be surprised how far V8 gets you with JIT optimizations - although serialization will be a bottleneck whether you're using JSON or deepClone.) For example, as one potential alternative, you could invent a scheme for encoding the state in a bit array, and then find clever bitwise operations for inspecting or manipulating it.

For inspiration, many chess engines store game state in a "bitboard" [0] which is a 64 bit representation of some (partial) state of the board.

Of course, at this point you might not even want to be using JavaScript... maybe you could write the hot path in Rust, compile it to wasm, and then use JS for orchestration/UI.

[0] https://en.wikipedia.org/wiki/Bitboard#Chess_bitboards


That is really useful, thank you! I think first stage is building the game and see if the whole Genetic Algorithm/Monte Carlo Setup actually works, even if the first few iterations take weeks.

The issue is I want to figure out if there are combos that I don't even know exist that are unusually strong. Basically make sure I balance the card pool but automatically.


> first stage is building the game

Yeah :) Definitely better to make it slow but ship it. Then make it fast later.


There are at least two obvious alternatives to (fully) deepcloning.

First, many "everything must be immutable" libraries provide data structures that give operations that feel like mutations (but actually are not), and many of those can re-use all the old objects that are not mutated. This could save a lot, relative to full naive deepcopies. Or it could save very little, depending on what you're doing.

Second, if you have the ability, you can always mutate before recursion and then revert when the recursion finishes. This could potentially be very tricky, because it's not always easy to revert mutations. As such, it's not a good plan for a prototype, but if you can get it right, this could make your tree search wayyyyy faster.


One use case I have, which I do not like, but there is literally no other way:

Communicating with workerthreads.

I so much would like to pass references of big readonly objects towards them - but no, full copy it is, for every thread.


You're probably aware of this, but in some cases you might be able to use SharedArrayBuffer [0], which doesn't make a copy of the underlying data but does use structured cloning to clone the outside of the object.

[0] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...


I only used it for experimentation so far, but I abondoned it, after I learned, that only one worker can be the active user of a SharedArrayBuffer. So they cannot be shared at the same time? Sharing seems to mean, now A uses it, than gives control back, via the main thread, then B uses it, etc

(but I hope I missunderstood something there)

But I need many threads to access the same data, at the same time (readonly, so no racecondition).


Are you sure about this?

You might be mixing ArrayBuffer (which is a transferrable object[0] and gets zero-copy moved) and SharedArrayBuffer.

From [1]:

> The structured clone algorithm accepts SharedArrayBuffer objects and typed arrays mapped onto SharedArrayBuffer objects. In both cases, the SharedArrayBuffer object is transmitted to the receiver resulting in a new, private SharedArrayBuffer object in the receiving agent (just as for ArrayBuffer). However, the shared data block referenced by the two SharedArrayBuffer objects is the same data block, and a side effect to the block in one agent will eventually become visible in the other agent.

[0] https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers...

[1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...


"Are you sure about this?"

No. And I hope to be wrong.

I will give it another try then, but I was using SharedArrayBuffer and I was getting explicit errors about this limitation.


Did a quick test and seems to work as expected: https://codesandbox.io/p/devbox/sharedarraybuffer-33qywc (see console in https://33qywc-3000.csb.app/)

Caveat is you need these headers for security reasons (hence why the sandbox above has Express) or SharedArrayBuffer is not even defined (at least in Firefox):

    Cross-Origin-Opener-Policy: same-origin
    Cross-Origin-Embedder-Policy: require-corp


Ah, the OnlyChild implementation of Shared.


An object containing just data for sure having lots of usecases. Like i.e undo/redo. But deep cloning would be as simple as json.parse(json.stringify(object))


Yeah, agreed. And in that case you should treat the "data object" as opaque, in which case it should be sufficient to pass it by reference. Hence, if you're reaching for deepClone, it's a code smell - why can't you pass the reference? Are you mutating the data object somewhere that you shouldn't be?


The saving grace here is that it destroys classes. I have often seen deep clone and deep equals as a sort of back-door API that then breaks when the implementation of the class does. The fact that this ignores prototypes makes it clear that you should only be using this for data, not "OO Objects".


React needs a new object for its hooks when you change them. We need to modify those objects, so deepClone + direct mutation on the clone bypasses a lot of annoying immutable object creation bs. ppl just destructure the whole thing anyways, which is actually also pretty slow


React hooks don't need to add the entire object to their dependency array; they can specify only the properties that are actually dependencies of the logic contained in the hook.


You should check out immer for that use case ("modifying" an immutable object with imperative code).


In my circles, "immutable everything" was from the functional purists.


I used it mostly when I pass an object into a function I don't control, but I want to make sure that the original object won't be changed.


Popular opinion* at least amongst the JS developers I’ve worked with for the last 10 years or so.


I agree that its weirdly difficult but if you are trying to deep clone an object, you are probably doing something wring


Not at all. It’s common to have nested data, it can be quite simple, but still nested. Think: config objects, simple state objects. I want my functions to be pure. Instead of modifying the nested object, they should return a modified deep copy.


Use the spread syntax to make a shallow copy; it's more performant.


This posts advice isn’t good because structuredClone performance is poor. Theres a small custom function I use for this which has much better performance.

If you’re so inclined you can google the benchmarks.


The article states differently, saying it is performant.

Instead of saying we should search for benchmarks why not post them here? Also, what is the small function you use?


Sadly I can confirm, structuredClone is still not performant. At least on my various devices.

I just did a test again with 40000 times cloning a simple object and JSON.parse/stringify was 6 times faster than structured cloning.

Since I heavily use deep cloning, I am still disappointed with the new native, but slower solution. (And was hoping there would be more than old news in the article)

Edit: Code for testing was as simple as

for ... 40000

JSON.parse(JSON.stringify(obj))

vs

structuredClone(obj)


> I just did a test again with 40000 times cloning a simple object and JSON.parse/stringify was 6 times faster than structured cloning.

Depending on the object you're cloning, that might be fine. But some types, like Date or Set, don't round trip between JS and JSON so you'd have mangled objects.


Yes I know, but 6 times slower is pretty bad, so when someone presents the modern way, he should probably include a warning, that modern not necesarily means better. Because those who used the old way before, know the limits and worked around them. So why should I switch? I wasted time trying it out (not now, when structuredClone came out). Because my assumption was, it should be faster than the JSON abuse. But it is not, it just has more features.


"Theres a small custom function I use for this which has much better performance."

Can you enlighten us?

JSON.parse and stringify, or customly copying what is needed? (the fastest solution to my knowledge)


My own version is proprietary so I can't release it but here's a reference and something like it:

https://www.measurethat.net/Benchmarks/Show/17150/0/lodash-c...


What types of objects are you copying?




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: