JavaScript private class fields considered harmful
lea.verou.me
lea.verou.me
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.
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.
This effectively and automatically devolves to "public-by-contract". Seen it happen so many times that you can't convince me otherwise.
https://github.com/tc39/proposal-function-implementation-hid...
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...
_checks first issue_ yep.
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.
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.
The backend was written in Go; everything could be updated to the latest version without any incompatibilities at all.
People having been complaining about incompatibilities in Rust too, but Rust is a much newer language, and I think things have settled by now as well. I don't know much about C#.
I'm not saying the problem doesn't exist, or that's certain parts of the ecosystem haven't experienced more churn than others. I am saying that "js churn" has been a meme joke for a long time, and has been old for almost as long.
I'm not so sure about that; things haven't really changed that much. A language gaining new features doesn't typically break everything, even when libraries start adopting them.
But even if it does: sure, languages can have their "everything changed" watershed moments, but no such watershed happened in JS in the last few years, yet updating things is still a lot of effort, and a bit of a pain.
It actually has nothing to do with the language; it's just the mindset. For whatever reason compatibility is just not high on the list of priorities in the JS ecosystem. I maintain Go packages that have been compatible for over 10 years; I make special effort to ensure they remain compatible, and even if a compatibility break is required I try to minimize it. There is nothing in JS or npm preventing the same kind of mindset.
Either way, it's not a "meme joke" and it's certainly not "old" as the problems still exist today. If you (or someone else) would say "I think the advantages of moving faster are more important than remaining compatible": okay, fair enough; I can see that's a reasonable point of view, even though I personally don't agree with it. What I always find off-putting is the casual dismissal of the problems. The Python 3 transition could have been handled better and dependency management remains a bit of a mess in Python: I don't see many Python developers deny these are (or were) real problems.
Rust appeared 7 years ago. Rust did not went through any churn. Arguably, the first iteration hasn't even ended yet.
They really aren't. Editions are just a mechanism to allow Rust developers to push backwards-incompatible changes as opt-in features. I repeat, Rust hasn't even went through its first iteration, and it shows a fundamental lack of understanding and insight to argue otherwise.
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.
Protected plus subclassing is just poor man's dependency injection; entirely dispensable.
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.
This is not true. Private members are a part of a class interface, while local/internal function/variables are not. They both serve totally different purposes. If you're using private members as a substitution to local/internal members, you're doing it in ways that are fundamentally wrong.
I know it sounds bad. Not my fault
Behavior is the interface/messages supported by the object, and data is an implementation detail that should not leak.
What's wrong with that?
And why are you pinning the blame on a whole paradigm from a problem caused by how a feature was implemented in a language renowned for being fundamentally broken ?
My question is, why is that necessary? Exactly what is the benefit, except for a tiny bit of convenience? Why is that better than functions that work on well-defined types? It adds coupling, and coupling adds complexity, so if I'm adding complexity, I want to know why it's better before I add it.
> And why are you pinning the blame on a whole paradigm from a problem caused by how a feature was implemented in a language renowned for being fundamentally broken
I think calling the JavaScript/TypeScript of today fundamentally broken is clearly untrue. But anyway, I'm not condemning OOP entirely because of this one feature in one language. I thought my comment made clear that I see this as just another in a long line of complexity I've seen that's caused by this unnecessary coupling of data and behavior.
When I think of the OOP paradigm, I really think of Java, which I would call more fundamentally broken than JavaScript. It's improved over the years by pulling in ideas like lambdas that loosen the straightjacket imposed by its broken interpretation of OOP. Half of the revered "design patterns" that we're supposed to learn for OOP are just to work around this coupling when it's getting in the way. Patterns, even if they're repeatable, are complexity. Have you implemented the Visitor Pattern [0] lately? I did it once and I will never do it again. That kind of stuff ties my brain in knots. If you like pointless puzzles, maybe it's fun, but for me it's just more and more complexity. When I just work with data and functions, most things become much simpler.
[0] https://en.wikipedia.org/wiki/Visitor_pattern#/media/File:Vi...
The benefit is subtype polymorphism via dynamic dispatch. When values flow through the program they carry the implementations of their operations with them, so a caller can operate on a value without knowing how that operation is implemented; a newly-introduced value can bring a new implementation of an existing operation, and existing callers can use that implementation without knowing that it’s new. There are other ways of achieving this, but OOP is a good one.
Subtyping/inheritance is sometimes convenient, but inheritance tends to be a constrictive way to reuse code that increases cognitive load. If B is a subtype of A, now, not only are the data of B coupled to the behavior of A, but the data or behavior of B _may or may not_ be coupled to A depending on whether B overrides A. More coupling. More cognitive load. And if you change A, you have to be careful that you don't break B. Then if you have C that's a subtype of B, then the cognitive load and possibility of unexpected breakage gets even higher. Ugh. No. I'm a simple-minded person. I don't want to think about stuff like that.
I see dynamic dispatch simply as a form of delegation, which can easily be accomplished functionally.
So, ultimately, again, I see these benefits as being _convenient_ but not _necessary_. And since all design decisions are trade-offs, I look at the downsides of the coupling and find that the trade-offs are simply not worth it.
Point us to a language that isn't broken in some way, an example of perfection that every language designer should aspire to follow.
And then show us how that perfection has helped that language to take over the computing world.
Caches which require specific invalidation logic.
A good mental model is the query language for a database. The language provides a frontdoor for all the state, so you can't just poke around and mutate things.
I'm not sure where ORMs entered the picture, but my point is that the database uses the query engine to guard internal caches which are private via the process boundary.
OOP is a technique which shouldn't prescribed for everything. I will say Javascript shouldn't have have classes, but I hope to never need to use JS much beyond glue.
The problem is more one of, you don't want everything to be a state machine, and adding the boilerplate tends to overcouple the data. The maximally composable form will tend to extract a little FSM with one variable, while all other relevant data is simply passed into every method.
Building in code APIs for libraries.
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.
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.
I never understood why TC39 was so intent on this “hard privacy” stance they took, as without it there were much better and more sensible compromises that could have been made. (Especially since JS is an interpreted language - if someone wants to work out what private variables are in your code, they can always just inspect the source.)
Records and tuples for example aren’t controversial. They are primitives and therefore solve a few quirks that come from objects. They should also radically improve performance because users can’t change types or add/remove properties causing less optimized lookups.
Instead, all that time went into a controversial feature that has made things worse without solving any real issues either.
Still, it's interesting to me that Proxy is more valuable to author than private member variables. Curious about some common use cases.
Using typescript to handle things like access modifiers at compile time makes everything much more predictable.
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.
One of the things that heavy use of JS frameworks will do is make you forget you can pass an object interface to represent continuations. The DOM spec defines the EventListener type, in TypeScript notation, as:
`(event: Event) => void | { handleEvent(event: Event): void }`
What does this type signature instruct us to think?
That a function closure should reference objects in its scope, no need for `this` binding, or an object interface, appropriate to the domain of the deferred caller should be referred to and called (e.g. `handleEvent` in the case of DOM's EventTarget interface, but one could also imagine `{ onTimeout() : void }` if the spec authors were consistent, perhaps accepting a key to differentiate calls in the same vein as event types), also eliminating the need to bind `this`. The addition of `WeakMap`\`WeakSet` to the runtime eliminates the need for manual resource management.
But React, the most popular JavaScript library (and its not alone in this), makes this pattern impossible: you can only pass functions to event listeners. So everyone then growls about `this` binding.
That was true long before classes arrived.
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.
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?
I meant the 'this' thing - a sibling comment to yours contains the correct code.
let m = obj.method
let res = m(arg)Thank you.
I've stuck with TypeScript private fields which does exactly what I need.
In TS, i default to protected for internal implementation details.
Why would one hinder future extenders by default?
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.
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.
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.
while that is always possible, it is not always reasonable.
If you take firefox as the example, the removal of the XUL extension api is one such. And while there's many a forks to keep the old XUL api, none of them really caught on, and so support for them such as maintaining security patches etc fails.
Imagine you have a type that represents some domain state. It has a bunch of fields that are used to represent this state. But that state has invariants. There are certain combinations of values that the fields are not allowed to take and they relate to each other in complex ways. A well designed interface to this type is such that every sequence of methods called on one of these objects will maintain the invariants. If you instead just directly expose the fields then absolutely nothing guarantees that the invariants hold.
Yes, if the first thing you do when you create a private field is write a public getter and a public setter then you haven't gained anything. But the power is in not writing these methods.
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.
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.
That's a reasonable commitment to ask for if you're poking around in implementation details.
How are you not taking the burden of maintenance when writing code that uses private-by-convention properties? The difference seems pretty abstract, a fork will require the same rework when internal properties change.
Adding a broken feature that was widely criticized for years (more than any other JS feature before it was added — including type coercion) just so some maintainers don’t have to tell users no about one particular thing isn’t a great idea.
It’s even less of a great idea when you consider that modules exist. Add a variable without marking it for export and you’re good to go.
Javascript modules do not work particularly well as namespaces.
You may `import * as Foo from “foo”` to create a closed namespace.
function(param1, param2){
return module Name {
import {param1} from super;
export let A = param1 + 1;
// This line would cause an error as
// param2 is not imported from the enclosing scope
// export let AA = param2 + 2;
}
}If I want to create new pieces of data, I have to make an allocation with a constructor. Reusing methods provided by modules is effectively the same as classes (the two being mathematically equivalent, but closures not having all the footguns).
This is true no matter if your code is OOP, imperative, or functional.
Well, in a neighboring comment someone is suggesting making the private stuff local, so it cannot be called as well. So your problem would persist just as well.
Without private methods or some equivalent you won't be able to change your library without breaking lots of people's code.
It also makes understanding the interface hard, if everything is public, how do I know which methods are intended for me to consume, and which are internal helpers.
Of course, not all interfaces will suit your need and some are poorly designed.
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.
The proxy problem is bigger than the “people don’t respect my authority” problem that led to private fields in the first place.
Nothing like having to reimplement dozens upon dozens of methods all because 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.
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…
The reality is that proxies came first. They see a LOT of use in reactive libraries making it very likely that your classes get passed into one these proxies (and breaking if private fields are used).
They knew all this when ramrodding it through over the many protestors warning about this and other things. The fault is 100% on the hubris of the spec committee.
The spec committee gave the community the middle finger and did it anyway.
Now we’re stuck with yet another giant footgun from their hubris.
Because it’s so new, maybe they should just consider removing it from the spec.
-- https://en.wikipedia.org/wiki/Self_(programming_language)
> JavaScript is a high-level, often just-in-time compiled language that conforms to the ECMAScript standard. It has dynamic typing, prototype-based object-orientation...
Compilers > VDOMs + runtimes
There’s also a case that using those methods is indistinguishable from something like typeclasses which don’t involve classes at all.
If we want to be even more pedantic, JS is prototypal and fundamentally lacks classes with objects simply having an optional pointer to B other objects, so the implementation of these methods doesn’t necessarily imply classes.
You also have an argument that proves too much. If this argument is true, then your previous multiparadigm claim is proven false.
In any case, it doesn’t seem like a good faith argument. There’s no reason I ever have to make my own classes in order to make projects in JavaScript. This is all a typical programmer is going to care about and is the spirit of my statement.
OOP isn't classes, I suggest education on OOP type theory, and regarding type classes, a few Symon Peyton Jones papers on how type classes in FP relate to OOP.
ACM and SIGPLAN.
Apologies in advance if I'm misunderstanding the purpose of proxies.
It's borederline clickbait, or selfbait, as the author believes they've contributed to the ever shuffling stinky pile of hot takes that exist in the web world.
It's really not as exciting to say Proxy, a niche class, than saying there's a problem with a concept of something every day, like private fields.
I doubt the vast majority of you have any experiene with Proxy, but OOP? Im sure you've brooded on a few hottakes about that in your careers.
JS has modules that offer variable privacy that doesn’t cause all the issues of private fields.