[HN Gopher] JavaScript private class fields considered harmful
___________________________________________________________________
JavaScript private class fields considered harmful
Author : feross
Score : 93 points
Date : 2023-04-28 21:47 UTC (1 days ago)
(HTM) web link (lea.verou.me)
(TXT) w3m dump (lea.verou.me)
| eyelidlessness wrote:
| The article (and author's referenced tweets) center on private
| field access failures with Proxy, but (and) the same problems
| arise if you override subclass members which defer to their super
| for private access. This is something that works _perfectly fine_
| with the `private` keyword semantics in TypeScript which predate
| runtime private class members. I agree that the actual runtime
| behavior is harmful, and I will only use it very carefully with
| very skeptical consideration of whether there's any value in
| using it.
|
| Fortunately symbols provide exactly the same runtime
| encapsulation semantics with none of the footguns. It's a little
| bit more ceremony, but you can achieve everything a private class
| field would accomplish by using singleton symbols as long as you
| don't export them or otherwise expose them to a consuming scope.
| mst wrote:
| Database appears to have gone _poof_ but somebody archived it
| beforehand: https://archive.ph/ffvTF
| migf wrote:
| Yyyyyeah, classes in js are pretty bad full stop. For instance,
| passing a method as an argument will do awful things unless you
| bind it to a this first. Gross.
|
| Still, it's interesting to me that Proxy is more valuable to
| author than private member variables. Curious about some common
| use cases.
| eyelidlessness wrote:
| Proxy is commonly used to wrap reactive object values, which is
| the use case the author identified as an issue.
| graypegg wrote:
| Totally agree. This is very much a symptom of JS's class-ish
| syntactic sugar. Access modifiers have to be a runtime check,
| and be bolted onto an environment that was built around
| function scope being the only way control access.
|
| Using typescript to handle things like access modifiers at
| compile time makes everything much more predictable.
| dexwiz wrote:
| Doesn't typescript just transpile to closure based access
| control?
| nosianu wrote:
| TypeScript's "private" is only for type checks. It is
| simply _removed_.
|
| You may also want to read what I wrote a few days ago
| already: https://news.ycombinator.com/item?id=35653653
|
| TypeScript now supports both the ECMAScript #private - it
| has no choice in the matter, since it's in the language now
| - and their own development-time "private". It sounds messy
| but that's the fault of those who wanted it _in the
| language_ , as runtime checks.
|
| I agree with the other comment, closures are far more
| powerful and flexible. I understand that this OOP stuff in
| JS was probably a nod to the many programmers in many
| companies already used to it, to make JS more palatable for
| them. It's easy to demand millions of people worldwide to
| adapt, but in business reality it does not work that way.
| So I grudgingly accept those new(er) things in JS as a
| business decision and necessity, for the huge crowd of
| people doing programming mostly just for the money, who had
| to write more and more code for the web that used to be run
| as installable software on Windows.
| afiori wrote:
| Closures are a much better "class" primitive than JS
| classes
| dexwiz wrote:
| Even without classes there are a ton of footguns involving
| 'this' and method scope. Fat arrow notation papered over a
| bunch of issues. But I have lost more than a few hours confused
| as to why changing between map and a loop completely broke/fix
| behavior. Or seen codebases absolutely peppered with unneeded
| 'bind(this)' like it would ward off evil. I had to spend some
| time studying before I really grokked it.
| pwdisswordfishc wrote:
| > For instance, passing a method as an argument will do awful
| things unless you bind it to a this first.
|
| That was true long before classes arrived.
| mst wrote:
| It continues to sadden me that the implicit this injection
| happens only on direct invocation - let res
| = obj.method(arg);
|
| and let m = obj.method; let res =
| obj.method(arg);
|
| being different things seems like an unfortunate trade-off
| (though not knowing the full reasons it was chosen to be that
| way makes me wary of assuming it was the wrong decision
| overall).
|
| I've been known, for things where the object is e.g. a bag of
| components, to loop at construction time and replace all of
| my methods with pre-bound copies of themselves, but having to
| make a conscious decision as to whether I need to introduce
| that extra code (and work for the runtime per-instantiation)
| still makes me grumble to myself every time.
| llamaLord wrote:
| Can you explain wrist you mean by those two examples being
| different things?
|
| Do you mean that 'res' evaluates to a different result in
| each?
|
| Or that when you call 'object.method()' vs calling 'm()'
| you would get a different 'this' context?
| auggierose wrote:
| Interesting read. I am drawing the opposite conclusion: I should
| avoid both Proxy and Vue.js!
| andrewstuart wrote:
| Private is a terrible concept.
|
| Only two days ago I chose to rewrite a chunk of code simply
| because I couldn't call a private function that did precisely
| what I needed.
|
| Also "considered harmful" is an overused cliche I wish people
| would stop saying it.
| n2d4 wrote:
| > Only two days ago I chose to rewrite a chunk of code simply
| because I couldn't call a private function that did precisely
| what I needed.
|
| It may have done what you needed in the current version of your
| library, but just look at it from the perspective of the
| library author. If they ever want to refactor, or maybe remove,
| that private function, it would break a lot of people's code.
| And even if it were marked private or deprecated, it would
| still break a lot of people's code. You could say "but if your
| code gets broken it's your own fault" and yes you're right, but
| it wouldn't stop those people from loudly complaining to the
| author.
| realusername wrote:
| What's the alternative then? Maintaining a fork? It's the
| same update problems just with more hassle.
| Nullabillity wrote:
| Yes, maintaining a fork means that you're explicitly taking
| on the maintenance burden from that point on, and are on
| your own in terms of updates.
|
| That's a reasonable commitment to ask for if you're poking
| around in implementation details.
| dimal wrote:
| This just reinforces my bias that OOP is what is really harmful.
| Really, what good reason is there to have data and behavior bound
| to the same object? I've never seen a good argument for that.
| It's just taken as a given. It seems nice and convenient, but
| that convenience seems to always lead to unexpected complexity
| like this.
|
| A while ago I was looking for a color manipulation library, and I
| saw her Color.js, and loved all the features. Then I saw the
| bullet point, "Readable, object-oriented API". Nope. I'm not
| touching that. I chose culori instead. It's just a collection of
| functions that transform standardized color data objects. It's
| like lodash for colors. It's been great to work with.
| ActorNightly wrote:
| >Really, what good reason is there to have data and behavior
| bound to the same object?
|
| Building in code APIs for libraries.
| lioeters wrote:
| I agree. Private class fields seem totally unnecessary when you
| can simply "hide" functions and variables in a local scope and
| not export/expose it. (Or use symbols as another comment
| suggested.) I also don't want a library for color manipulation
| to be using Proxy and other magic tricks, including "this". I
| prefer plain functions that take any values they need as
| explicit arguments, and simply return results - without
| maintaining state, having side effects, pretending to be a
| plain object, etc.
| SSchick wrote:
| Personally I feel like private JS class fields are against the
| spirit of a scripting language and are harmful to debugging and
| inspectability (in the way they are implemented).
|
| I've stuck with TypeScript private fields which does exactly what
| I need.
| zeroCalories wrote:
| It seems to me that private fields are a feature best handled by
| a compiler. Such a runtime check will only be useful in very
| limited situations.
| i386 wrote:
| Isn't the point that private fields remain private implementation
| details of the class in just about every language that has them?
|
| As a library author, if I declare something as private, it means
| you the consumer, may damage internal state in some way by
| directly mutating those fields.
|
| If they do need to be read or mutated, that's a good case for
| adding to the public API, not complaining that reflection tricks
| can't get at something you shouldn't have access to. Not to
| mention how incredibly fragile your consumer is going to be when
| upgrading... I feel like the JS folks are learning the same
| lessons the hard way that the Java folks did 25 years ago.
| jmathai wrote:
| Isn't most Javascript rendered useless by the time it is ready
| to be upgraded because all of its dependencies have moved on to
| not so greener pastures?
|
| On a serious note. Keeping up with changes in the Javascript
| community seems like a full time job. I don't know how they get
| any work done.
| zdragnar wrote:
| This joke was already getting old 6 years ago.
|
| The major frameworks of today were already well established
| then, and the last really big update to the language in 2015
| had already been in fairly wide spread use via transpilers
| (traceur and later 6to5, which became babel).
|
| Honestly, in the last 13 years, many mainstream languages
| have gone through similar churn. JavaScript has gone through
| more than most, to be sure, but nobody complains about Rust
| or C# nearly so much.
| beezlewax wrote:
| JS is dependencies all the way down. Upgrading things can
| be a nightmare
| bilalq wrote:
| And yet, I find myself struggling way more with
| Objective-C/Swift and Java/Kotlin deps than I do with JS
| ones. Dependency upgrades can be problematic, sure, but
| JS is no worse than any other language. I never had to
| change compiler flags or mutate internals of a library in
| npm the way I need to do with cocoapods.
| krono wrote:
| There is actually a stage 2 proposal "Function Implementation
| Hiding" that is being pushed through despite the many very
| legitimate and thoroughly described concerns that people have
| been voicing on GitHub and everywhere else this topic is
| brought up.
|
| https://github.com/tc39/proposal-function-implementation-hid...
| pwdisswordfishc wrote:
| What are the concerns, actually? I have seen this proposal
| before, and I don't recall any significant concerns.
| krono wrote:
| A quick glance over the GitHub issue titles should give you
| a general idea.
|
| Whilst I don't personally share all of the concerns posted
| there and even believe some of them are a little bit silly,
| I have to say that I do not at all like the way most of the
| discussions are abruptly cut short or simply left
| unanswered, and how difficult questions or ideas that
| aren't entirely in line with the original proposal are
| immediately declared nonsensical/invalid or shot down
| without any further exploration at all.
|
| The above is a common theme in that repo and not difficult
| to find. I've never seen that kind of behaviour in any
| other proposal at that stage. Very sad to see.
|
| https://github.com/tc39/proposal-function-implementation-
| hid...
| zdragnar wrote:
| Eh, most of the time, if you use private, what you really
| wanted was protected.
|
| Private causes extensibility issues (no access to private
| fields in child classes). Not sure if the article addressed
| this or not because it's currently hugged to death.
| mst wrote:
| https://archive.ph/ffvTF
| SwiftyBug wrote:
| It does.
| pwdisswordfishc wrote:
| No, most of the time, if I use private, what I really wanted
| is private. And it does exactly what I want. So speak for
| yourself.
|
| Protected plus subclassing is just poor man's dependency
| injection; entirely dispensable.
| iudqnolq wrote:
| The article seems quite practical.
|
| She wants her users to be able to use Proxies over her classes.
| Private members make a class ineligible for proxying. So she'd
| rather use private-by-convention.
| lenkite wrote:
| "So she'd rather use private-by-convention."
|
| This effectively and automatically devolves to "public-by-
| contract". Seen it happen so many times that you can't
| convince me otherwise.
| pwdisswordfishc wrote:
| Proxies exist pretty much for one purpose and one purpose only:
| to implement membranes, like described in
| <https://github.com/ajvincent/es-membrane/>, where you build on
| demand a whole parallel universe of proxies that stand in for
| objects existing somewhere else, and have wrapper functions
| convert between those proxies and real objects. Any other use is
| incidental and unsupported.
|
| Admittedly, not many people know this, so those who are not
| deeply familiar with TC39 proceedings commonly think of proxies
| as just a way to hook into arbitrary behaviours of a single
| object, like property accesses, then are treated to a bitter
| surprise when proxies don't work like what they thought or
| wanted.
|
| Still, the opening example is simply a misuse of Proxy, so it's
| rather unconvincing.
|
| That said, with something like
| <https://github.com/littledan/proposal-new-
| initialize#another...>, something resembling the popular
| conception of Proxy might actually be made to work...
| NoahKAndrews wrote:
| Can you elaborate on what's wrong with using then for other
| purposes?
| 8bitsrule wrote:
| Great thing about JS is you _never_ need to use any OOP.
| afiori wrote:
| Personally this only reinforces how JS should have never gotten
| classes.
| rektide wrote:
| In general, the people writing language features out of fear
| cause the bulk of the problems.
|
| Js had coped quite well without having private fields. We had
| conventions of using this._field that served fine, that cautioned
| users sometbing was internal-ish without binding them, without
| preventing experimentation.
|
| The private function implementation is more bad language design
| seeming on an inevitable course to make the language worse & less
| capable. It's more fear, more adding nothing, more creating
| friction, like private fields.
|
| That all said, it doesn't seem like proxies ought be broken by
| private class fields. That's another heap of scorn placed upon
| this already tragic & harmful misfeature.
| claytongulick wrote:
| I was amongst many who argued vocally against this proposal
| back when it was in stage 1.
|
| There was a huge community backlash against it, which tc39
| blithely ignored.
|
| It made me lose confidence in tc39 completely.
|
| Whatever their motivations are, they are clearly not influenced
| by community feedback, at least not for this feature.
| transfire wrote:
| Had similar problems with Java developing for Android.
|
| Nothing like having to reimplement dozens upon dozens of methods
| all because you need one little change in one untouchable private
| method.
| sbdkrend wrote:
| > you need one little change in one untouchable private method
|
| Tell me you don't know how anything works while not saying you
| don't know how anything works.
| peter-m80 wrote:
| I just avoid classes in js
| nailer wrote:
| Indeed. They're very much implemented for people that wanted
| nineties style OO and who don't get scopes/closures.
| 8n4vidtmkvmk wrote:
| whahaha. i took the exact opposite approach and swore off proxies
| years ago for exactly this kind of edge case. there's scenarios
| where they just don't work and they'll bite you and i don't want
| to invite more gotchas into my code.
| afiori wrote:
| The author is not interested in writing proxy code, their
| interest is to allow other to use their classes together with
| libraries that happen to use proxies.
| jschrf wrote:
| I can't recall a single time in JS/TS, C# or even Java where I
| went "phew, glad i made that private!".
|
| In TS, i default to protected for internal implementation
| details.
|
| Why would one hinder future extenders by default?
| SigmundA wrote:
| If I am understanding the issue here I don't believe C# would
| suffer from this issue since reflection works not only on
| public method access where those methods use privates
| internally but in fact could access privates directly.
|
| In C# private is more of suggestion than an enforced concept
| since you can do anything you want with reflection, which is
| very useful and necessary sometimes. Keeps normal code clean
| and encapsulated while allowing one to go under the covers as
| needed.
|
| Seems very strange that not only can you not get to private
| members directly in JS but even worse you can't call a public
| method that use a private though a proxy, bizarre and against
| what I would think JS to be.
| thfuran wrote:
| Unless you're going 100% immutable everything (which the
| language isn't great for), advocating for making all members
| public in java seems roughly as sane as advocating for
| declaring all variables and returns as Object.
| oweiler wrote:
| Because extensibility should be a design decision.
| mst wrote:
| That feels like an argument for 'final' more than 'private.'
|
| Personally, at least for library code, I tend to assume
| -somebody- will extend any given class of mine at -some-
| point and would prefer they had a reasonable time doing so,
| but I use 'prefer' advisedly since there are absolutely
| situations where doing so would almost certainly be a footgun
| and in that case I'd rather dissuade the user before sinking
| time into the idea.
| chii wrote:
| extensibility not thought of during design is a flaw. which
| can be patched over if the actual internals are reachable.
| everforward wrote:
| Sure, but extensibility often comes with complexity. If I'm
| making something non-extensible it's to keep things simple
| or to prevent a bunch of bugs around people trying to
| extend something with complicated state.
|
| They always have the nuclear option of forking and making
| those fields no longer internal. They'll have to take on
| the maintenance overhead that I shirked, but that should be
| fine if they need it that badly.
___________________________________________________________________
(page generated 2023-04-29 23:02 UTC)