9 ms·
Deep cloning objects in JavaScript
- twic 3y agoNot cloning instances of classes properly is a surprise. Is there a particular reason for that?
- KTibow 3y agoClasses have way more complex state that could be unpredictable
- jwalton 3y agoOn 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).
- eyelidlessness 3y agostructuredClone 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.
- toddmorey 3y agoI cannot believe lodash cloneDeep (just the one function) is 17.4kb minified!
- jwalton 3y agoA 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/baseClone.ts https://github.com/lodash/lodash/blob/main/src/.internal/bas...
- baq 3y agoWould it be shorter if it didn’t, though?
- addandsubtract 3y agoThe 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.
- qayxc 3y agoGlimpsing 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.
- mishac 3y agoUnnecessary _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
- nobleach 3y agoI 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.
- user3939382 3y agoThis 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.
- KTibow 3y agoWhat types of objects are you copying?
- binarymax 3y agoThe 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?
- hutzlibu 3y agoSadly 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)
- meepmorp 3y ago> 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.
- hutzlibu 3y agoYes 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.
- chatmasta 3y agoUnpopular 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.)
- holoduke 3y agoAn 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))
- chatmasta 3y agoYeah, 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?
- endisneigh 3y agoThere 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.
- chatmasta 3y agoYep. 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.
- xpressvideoz 3y agoTIL, 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.
- professoretc 3y agoIt 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.
- mike_hock 3y ago`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.
- jay_kyburz 3y ago"stringify" sound more like toString than serialize to me.
- anamexis 3y agoI think that is the issue.
- baq 3y agoI agree with everyone here: it’s named correctly, but other languages did it… differently. (Stopping short of “better”, but only just.)
- randomdata 3y agoThat 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.
- fennecbutt 3y agoAs 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.
- fshr 3y agoIt 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()
- deleted 3y ago[deleted]
- hughw 3y agoLooks like Webkit doesn't support it in workers. Does anyone know why that would be difficult?
- culi 3y agowell 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 https://bugs.webkit.org/show_bug.cgi?id=228331
- hughw 3y agoA screenshot of caniuse or similar is in the linked article.
- hughw 3y agoThe 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/structuredClone https://developer.mozilla.org/en-US/docs/Web/API/structuredC...
- johnfn 3y ago> 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-object-with-an-arrow-function-in-javascript https://stackoverflow.com/questions/51939812/how-to-clone-an...
- jauntywundrkind 3y agoI disagree that this is trivial. You have a function, and that function could have local bindings to arbitrary local variables.
- johnfn 3y agoI didn't say that the algorithm was trivial, just that the object was. `{ foo: () => {} }` is a fairly trivial object, all things considered.
- jauntywundrkind 3y agoIt'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.
- baq 3y agoFunctions are first-class objects though…? What’s json got to do with cloning objects except its prevalence as a limited workaround?
- eviks 3y agoWhy go with less recognizable "structured" in the name instead of "deep"?
- HellsMaddy 3y agoI 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 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/2015AprJun/0251.html https://lists.w3.org/Archives/Public/public-webapps/2015AprJ...
- eviks 3y agoThank you, have you seen whether there was any consideration of the obvious naming flaw? Don't see anything in these links
- HellsMaddy 3y agoI 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.
- n2d4 3y agoWhat is the flaw exactly? Or you mean that "deep" is just a better/more well-known name?
- shanghaikid 3y agoFor 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.
- noduerme 3y agoAgreed, 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.
- leeoniya 3y agofor those considering Immer, check out Mutative or Limu instead (both much faster) https://github.com/unadlib/mutative https://github.com/unadlib/mutative https://github.com/tnfe/limu https://github.com/tnfe/limu
- ramesh31 3y agoWait, what? When did this happen?
- Semaphor 3y agoApparently, 1-2 years ago. I was also surprised.
- namuol 3y agoIf 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-record-tuple - https://github.com/tc39/proposal-deep-path-properties-for-record https://github.com/tc39/proposal-deep-path-properties-for-re...
- nlitened 3y agoAlso, if one found their way to immutable Records and Tuples, maybe it's also time to start exploring Clojure/ClojureScript.
- namuol 3y agoMaybe. But I think there’s room to bring more ideas from Lisps into a language at least partly inspired by Lisps.
- user3939382 3y agoI 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.
- raspasov 3y agoJS: deep copy so awesome!! CLJS: ¯\_(ツ)_/¯
- Tajnymag 3y agoThe hash symbol looks really out of place in js.
- meandmycode 3y agoit may do, but it has prior use already: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_properties https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
- rabbits_2002 3y agoI agree that its weirdly difficult but if you are trying to deep clone an object, you are probably doing something wring
- WA 3y agoNot 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.
- lambda_garden 3y agoUse the spread syntax to make a shallow copy; it's more performant.
- ggorlen 3y agoI 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-clone-deep-code https://www.builder.io/blog/structured-clone#why-not-code-cl...
- deleted 3y ago[deleted]
- girafffe_i 3y agoAgreed
- lambda_garden 3y agoI've never had the need to clone anything in a real JS code-base, personally. What's the use-case?
- Gare 3y agoWhen 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.
- VPenkov 3y agoIt 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.
- lambda_garden 3y agoI use React. Why does that require deep clones? I simply do: return { ...current foo: "bar" };
- VPenkov 3y agoI'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.
- Drakim 3y agoif "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.
- RyanHamilton 3y agoI wasn't aware of this, I would like to give a shout out to superjson (https://github.com/blitz-js/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.
- kaliqt 3y agoActually 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.
- olooney 3y agoWow, 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/ https://www.oranlooney.com/post/deep-copy-javascript/ [2]: https://es.discourse.group/t/structuredclone-as-ecmascript-standard/1661 https://es.discourse.group/t/structuredclone-as-ecmascript-s...
- dgreensp 3y agoWell 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!
- ok123456 3y agoJust 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.