The reasoning behind "fields"...

I just want an understanding for 2 questions around the same topic: "fields". The first question is simple:

  1. If objects on a prototype object had been treated as copy-on-write to any portion of that object since long before this proposal, would class data declarations have been placed on the prototype instead of "fields"?

Put anther way, is the object on the prototype problem (foot gun) the primary (if not sole) reason behind fields? There have been claims that this proposal is just following the pattern laid down by developers using ES6 classes. While that may be true, the reasoning isn't the best given that programmers are going to take the simplest solution to a problem, and in this case, the problem was that ES6 classes didn't provide for declarative data.

What I'm trying to get from this first question is why we're walking down this awkward path in the first place. If the relationship between class data declarations and the prototype foot-gun is compared to the relationship between private-fields and Proxy objects, it can easily be said that where in one case, TC39 is willing to allow a newly created problem to exist because its "just an expansion of the scope of an existing problem", with the other the zeal to avoid expanding the scope of one existing problem is leading to the expansion of the scope of several other problems of lesser renown.

The second question is more technical:
2. What approaches, if any, were raised to directly tackle the foot-gun in question, and why were they not acceptable?

TC39 is a group of very bright people, so it would be completely outside my expectations for no one in the committee to make such a suggestion. If I'm capable of raising at least 3 viable solutions by myself, it must certainly be the case that similar solutions were tried and defeated before settling on a design with so many trade-offs for developers.

My only intent with these questions is to understand the line of thinking that went into this design. I will likely always be of the mindset that this design does not represent TC39's best work, and is not the best design that could be created either. However, I don't wish to argue over such things. I merely wish to understand how we got here.

So far, @bakkot has responded as such:

If objects on a prototype object had been treated as copy-on-write to any portion of that object since long before this proposal, would class data declarations have been placed on the prototype instead of "fields"?

Since in fact they were not so treated, I don't think the committee ever seriously considered the question.

I don't know. Maybe? It is hard to speculate about what people would do in worlds significantly different from this one.

It's not a world I've lived in or thought much about, so it's hard to say. And it would depend on precisely how things worked: for example, I'd suspect I would be in favor of prototype placement for fields it if all values in the language were fully immutable, but that would be a very different language (and the distinction between prototype placement and instance placement would matter a great deal less).

What approaches, if any, were raised to directly tackle the foot-gun in question, and why were they not acceptable?

Any such approach would, pretty much by definition, involve changing core semantics of the language, either by breaking backwards compatibility or introducing a new "mode" along the lines of strict mode - though it's not clear how such a mode could work, since objects can move between code in one mode and code in another. Both of these are widely regarded as bad ideas in general for reasons gone into at length elsewhere; I don't think this issue tracker is the correct place to rehash those.


That helps me for how he thinks. I'm also curious to hear from the others who had a say in this proposal.

For me, one thing is that not every field is an inline value that's copyable on write. Sometimes they're created based on other parts of the instance, that creation is expensive, and it can't be done in advance:


class C extends B {

a = this.something();

b = this.somethingElse(this.a);

}

The only way this makes sense is if they're methods - accessors or not - but accessors are a feature I strive to always avoid for many reasons, including maintainability, clarity, as well as performance, and I want the resulting values to be data properties.

Note that any sort of magic copy-on-write capabilities also would fall into the same bucket for me as accessors - confusing spooky action at a distance that's best avoided.

@ljharb After having had so many discussions with you in the past, I understand pretty well why you feel that way. All I'll say is that you shouldn't count on that way of thinking being in the majority when it comes to JS developers. That being said, I'd still like to hear your response to the two questions.

For number 1, it certainly seems reasonable that if JavaScript had always been a language with the different semantics you suggest, that the wider community would have consistently put data members on the prototype instead of in the constructor body, and then class data declarations would have naturally followed the same paved cowpath.

For number 2, I don't recall any specific technical approaches (but I believe that at least one was suggested), but I can't concieve of how any technical approach to solve this would have been able to also reliably apply to pre-ES6 style constructor function patterns - and the inconsistency both between class Foo {} and function Foo {}, as well as the idiomatic dissonance between "the way everyone's always done inheritance in JS" and "this new way of doing inheritance in JS", both seem like they'd be unacceptable to me, and I suspect I'm not alone.

@ljharb Thanks for your responses.

How you responded to #1 granted me a little more insight into the choices behind class-fields. If I understand correctly, "fields" is actually meant to declaratively model what developers have had to do in the absence of data properties in ES6 classes. Now the pattern of unfortunate decisions makes a little sense. They are what's necessary to maintain the semantic achieved through constructor initialization. Now instead of questioning the reason for the decisions, I can only question the choice to follow what for developers was the simplest path to achieving their goal. But that's a line of questioning for a later time.

For your response to #2, given the understanding I've gained from your response to #1, it's sad but not surprising. Since the goal was to simply model what was already being done, it seems it didn't cross many minds to explore too many other avenues. Reasonable, but unfortunate.

...I can't concieve of how any technical approach to solve this would have been able to also reliably apply to pre-ES6 style constructor function patterns...

If you wouldn't mind, I'd love to discuss that with you.... somewhere else.

To other members of TC39, I'd still appreciate your input as well.