80 comments

[ 2.9 ms ] story [ 152 ms ] thread
Kinda disappointing. I don't really like the # for privates. It's not an elegant solution. It feels a bit like a one-off hack honestly.
There's not really a better way while maintaining encapsulation.
Don't we already have solutions (closures/WeakMap) for hard encapsulation? Seems like '#' just pollutes the language and isn't really needed.
There are solutions, but people want a syntax solution for it. Personally, I think that private instance properties should be a separate proposal from static and instance properties. That way, people could decide if they want some of it, or all of it.
Right now I'm prefixing variables I don't want external actors to change with underscores, which doesn't actually do anything, so is arguably more of a hack. :P
You could encapsulate them with a closure. They don't _need_ to be exposed.
Does this scale in a large code base? What's the effect on memory and performance of hiding most of your fields by closures?
Closures in JS are basically objects that have only one public property (called apply).

Modern JITs are good at optimizing static objects (provided props are never added or deleted and the types never change). There is still (from what I remember) a small performance increase to normal objects, but it only becomes significant if you are creating hundreds or thousands of them.

I'd say that in typical cases though that closures should be faster because devs tend to use them in ways that are easier to optimize (Closures don't ever add/remove properties and types are much more likely to stay the same).

> Closures in JS are basically objects that have only one public property (called apply).

Never heard it explained this way. Can you elaborate a bit? I'm not sure I follow. Couldn't a closure have any of the same properties that other functions have? Call, apply, bind, length, etc, etc.

When you execute a function, you set the program to execute the new function, but first, you take care of the related bookkeeping. A very important part of that is the function scope(s).

I'll first explain prototypical inheritance (because the two have close parallels). When you access a property in an object, a method runs behind the scenes. It does something like: search all the keys in the given object. If there is a matching key, return the value. If no such key exists, check for a __proto__ key. If it contains a value, call this function recursively on that object (and return whatever it gives back). If there is no __proto__ value, return `undefined`.

Let's assume an interpreter (to make it easy). Before we call a function, we need to setup the closure. The closure is an object with the names of all the variables you defined plus a few builtin things.

As we parse the function, any params, `var` or `function` statement creates an entries in the object. The values for the params are then pre-defined from the stuff provided by the caller. All the function statements are also pre-compiled. We add internal values for the return value, the parent closure (we'll call it __closure__), and some other things.

As the function runs, we come across a variable name. We then call a method to get it. That method searches the current scope for the name and returns the value. If none is found, it returns the result of calling itself on the __closure__ object. If no such object exists, it returns a `ReferenceError`.

Modern JITs do many fancy things with closures (just like they do with objects), but this basic mental model should cover most things.

Can someone provide some context? This link just drops you into the middle of the argument.
Some in the JavaScript community have been asking for private members as a language feature and some folks do not want this as there is a subset of JavaScript programmers who worry about many concepts from OOP "polluting" the relative minimalism of JavaScript. A fear I share to a certain extent.

From reading the thread it sounds like some feel that the addition of private members is being accomplished by fiat rather than community consensus.

> From reading the thread it sounds like some feel that the addition of private members is being accomplished by fiat rather than community consensus.

There's a difference between "community consensus" and "consensus of the people posting comments on this github issue".

What evidence is there for the "silent majority"? There's no good way to collect that kind of data (after all, they're silent). It sounds like a baseless statement of "fact" to bolster an otherwise losing position.
Look at the harsh responses that we can already see on HN and you will understand why I usually prefer to remain silent on such topic. A lot JS devs seem to feel very strongly about any kind of change.
Seems like a pretty fair and well though out solution after reading the latest response by bakkot.
Agreed. I've been following the spec for a few months and while # is "ugly," it serves the purpose of the concept well and works with the current spec/implementations without much headache. Seems like a win-win to me and I haven't seen a convincing argument for why private # properties shouldn't move forward as they are.
I think the concern is that if you look here: https://github.com/tc39/proposal-private-fields/issues/14 and other places, you'll see this massive push back from the community, yet the proposal is moving forward regardless.
Gotta get into the enterprise somehow. Even if that means bloating the language so its easier for the career desk jockeys to use (ie: dont ask them to learn something new).
Pushback from the community means nothing to TC39. Zilch. Zero. Nada.

The only instance of something dropped was cancelable observables. AFAIR it was only because some other group at Google opposed it.

Everything else is carried full steam ahead. Enjoy your import() instead of the vastly superior System.loader. Enjoy your hashes as member access specifiers. Dumpster fire. Template literals. BigInts. `new.target`. Dumpster fire. `import.meta`. Dumpster fire

While it hasn't changed their minds, I do see the authors of the proposal actively discussing and engaging with the community. That's better than nothing, I suppose.
I read through the thread, and the proposal sounds crazy to me.

1. Members marked private being accessible from other instances of the same class [1] is counter to every other OO language I have used. What is the prior art here? Why depart from all conventions like this?

2. The # syntax is not extensible. If TC39 decides to add protected, internal, etc. modifiers in the future, what will they look like? We should be looking to TypeScript's private/protected modifiers as a battle tested solution that fits the existing convention set by the "static" modifier.

---

[1] https://github.com/tc39/proposal-class-fields/issues/15#issu...

In answer to (1) - I'm a bit rusty, but I believe Java allows objects of the same type to access other objects' private data. I definitely agree that it's gross, but it exists elsewhere.
> 1. Members marked private being accessible from other instances of the same class [1] is counter to every other OO language I have used. What is the prior art here? Why depart from all conventions like this?

C++ works that way, and several other languages do as well. And the use case mentioned in that comment is exactly the reason: the implementation of a class knows how to access both its own private fields and those of another instance of itself, so that it can do comparisons, combinations, or other operations.

It gets really tricky when you have any kind of polymorphism, intended or not.

If you accept instances of your own class as arguments to a method, you may be templated to think you can access the private fields of those arguments, even if instanceof checks pass, but they may not be there.

(comment deleted)
The answer to #2 is that you can just put a private / protected keyword in front of the `#prop` declaration. The difference between public and protected, etc is larger than the difference between private and protected, etc.
Downvoting this comment because:

1. Afaik every single OO language allows this. I wouldn't know of prior art for the opposite behavior (only allowing access to the same object).

2. TypeScript works under different constraints and - for all intends and purposes - doesn't implement private properties when it comes to the final, running program. The reasons why TypeScript isn't a battle test solution are laid out pretty explicitly in the Github thread.

The only mainstream language I'm aware of (having looked into this before because someone complained about it re: TypeScript) that disallows cross-instance private access is Ruby.
> Afaik every single OO language allows this.

Ruby and Scala disallow it, to name two (edit: to clarify, Scala supports both modes via private and private[this]). I stand corrected on Java, C#, and C++.

> The reasons why TypeScript isn't a battle test solution are laid out pretty explicitly in the Github thread.

I didn't see that, can you point me to it? Yes, in TS it's a compile-time-only check (as are all TS checks), but what does that have to do with reusing the same keyword for a runtime check?

> 1. Members marked private being accessible from other instances of the same class [1] is counter to every other OO language I have used. What is the prior art here? Why depart from all conventions like this?

Almost every statically typed OOP language works this way. In C++, Java, and C#, privacy is class-based, not instance-based.

Privacy is instance-based in Smalltalk and, as I understand it, has been a long-time source of frustration. It makes it very difficult to implement things like an equality method so that an object can compare itself to another of its own type without breaking its own encapsulation.

If you think about it from the perspective of software engineering (and not security, which is generally not what language-level privacy is for), instance-based privacy has no benefits over class-based privacy.

The goal of access control is to encapsulate regions of code from each other so that modification to one doesn't affect others. It establishes fences between different parts of the program to make them less coupled to each other and easier to independently change.

Every instance of the same class shares the exact same code, so there is no point in preventing access between them. It's not like you can encapsulate things such that a change to class A doesn't require a change to... class A. That's the same class that you're already touching.

> We should be looking to TypeScript's private/protected modifiers as a battle tested solution that fits the existing convention set by the "static" modifier.

I believe those rely on static analysis, which JavaScript does not have.

Javascript is a poorly-designed, committee-managed, machine-oriented intermediate language. Why should hackers care?
This is the primary reason I didn't like the idea of adding "classes" to JavaScript in the first place. Classical OOP doesn't really have a place here and all it does is invite the wrong type of thinking into the language.
Agreed. We already had solutions for inheritance, etc... using prototypes, which are more powerful than classic OOP. Now we're in sort of a mess hybrid thing where issues like this are mostly unsolvable in a clean way.
JS classes build on prototypal inheritance, they aren't an alternative mechanism. So you apparently either don't understand them, don't understand "classic OOP", or both.
Yeah, I've only been doing OOP for about 22 years, so I'm pretty new to it.

The point was that the syntactic sugar isn't needed.

The 'class' sugar actually obscures the power of prototypal inheritance, and imho, discourages people from learning it - to their, and the language's, detriment.

> The point was that the syntactic sugar isn't needed.

Yes it is. The success of CoffeeSCript is absolutely a validation that developers are more productive with syntactic sugar such as the class keyword, destructuring and many many other features.

Now nobody forces you to use them. So why complain? It doesn't make Javascript harder to read for you, quite the contrary.

> So why complain?

So it is your opinion that the community and users shouldn't give feedback on the design and direction of the language they use every day? That's an interesting approach to openness.

(comment deleted)
(comment deleted)
I honestly do not see the need for these bloated features being added to Js just to appease programmers who refuse to learn a differing methodology.
A noteworthy feature of JavaScript is that it is predominantly a scripting language for large OOP-designed C++ systems. All the major browsers are C++ underneath, and so is Node.

This has created an odd tension where the APIs that programmers use are implemented as OOP under the hood, yet JavaScript itself shied away from OOP concepts. With features like this, the JS standard is slowly creeping towards the style of programming that underlies the ecosystem.

True "Classes" in JS always seemed like a hack anyways. It will never be true OOP, so why shoehorn these concepts into it? Especially given prototypical inheritance patterns we will never have real java style oop patterns. Except for primitives, almost everything is an object, but they are basically hashmaps, not real classes or objects as someone from other languages might think of them.

Imo, the language was not build or designed for some of these heavier OOP concepts. Just my opinion.

This seems to fly in the face of many of the great things ES6 implemented.

You don't need all aspects of OOP ever to get use out of some of its concepts. ES2015 classes are much cleaner and easier for newbies to grok than messing with the prototype chain, which was the way that sort of thing was achieved before.

Classes also make a lot of sense for custom elements (Web Components.) See Polymer v1 compared to v2.

Es2015 classes are just syntactic sugar around prototypically inherited objects. I agree they are nicer to use but under the hood there's no change. Which is why adding all of these oop concepts seems contrary to how the language is designed. At the end of the day JS has no true classical classes.

Not an expert just my opinion.

Good point! I do think that private properties is a worthwhile endeavor though, even if it's more of a "hard OOP" concept that on the surface conflicts with the historical ES spec.
ES2015 classes are not easier to learn for newbies, they're easier to learn for Rubyists and other people coming from OO languages.

There's nothing hard about this:

    function Person(name) {
      this.name = name
    }

    Person.prototype.greet = function() {
      return "Hi, "+this.name
    }

    var me = new Person("Erik")
    console.log(me.greet())
The syntax hurts your brain if you're used to looking at something else, but the control flow is actually very simple and totally transparent, unlike ES6, which does magical things that can't be understood by thinking about where the thread is moving.
There's zero magic. It is literally syntactic sugar and nothing else. Calling that magic is dishonest.
Ok, I can agree and I'll tighten up my use of "magic", reserve it for Railsy stuff where there's a lot of cpu cycles behind the scenes.

It's an extra layer of indirection that you can't see in the debugger though. Not sure what to call that.

I think calling me dishonest is a little strange. How is poor word choice dishonest?

Which is easier, explaining to your hypothetical total beginner what "Person.prototype" is and going down the rabbit hole of prototype-inheritance, and explaining how Javascript functions (unlike just about every other language's functions) can have attributes on them and can act like fake constructors and what "this" means in your example (and then later what it means in a different case, since it means so many different things)...or simply saying "yeah, we can define a new category, or 'class', of things called Person by saying 'class Person { ....'"?
You only think "class" is self explanatory because you already learned what it means.
Yes, but plenty of other non-programmers also learned what it means, when they first heard the English word. So it's not a huge conceptual leap.

Whereas "prototype" has completely different English dictionary meaning, and thus requires in-depth redefinition.

The keywords were reserved in the latest spec for just this purpose.

I don't think the rationale for their inclusion is a good one. The inclusion of such keywords in languages like C++, Java, etc was well-meaning but a mistake in retrospect. In the presence of global variables and direct assignment these keywords are the PHB approach to information hiding. They require ceremony and diligence to achieve true encapsulation. It's a murky situation at best.

See Bob Martin https://www.youtube.com/watch?v=TMuno5RZNeE&t=2325 and Bertrand Meyer on Eiffel: http://se.inf.ethz.ch/old/teaching/ss2007/0050/slides/03_sof... (Bertrand Meyer coined the Open/Closed Principle and is the developer behind Eiffel).

At this point though I see their inclusion in JS being almost inevitable. Just another feature to ignore.

It'd be nicer if they would look to add syntax for higher-order operators like compose to the language instead of this.

Why are private properties so sorely needed? Ruby is an OOP language, and gets along just fine without it.
You can declare visibility in Ruby: https://en.wikibooks.org/wiki/Ruby_Programming/Syntax/Classe...

It can usually (always?) be gotten around though.

Right. You can say something's "private", but there's always a way to access it. As far as I'm aware, this has never been an actual problem, and in fact has been useful since you can "patch" a library if you really need to.
Admittedly I have barely used Ruby, but isn't the fact that those "attr_reader"s have to be declared explicitly an indication that the fields are private by default?
attr_reader :my_prop is just a shorthand way of creating a convenience method that returns the internal value of @my_prop. But in any case, there is always a way to access a Ruby object's internal instance values. See my other comment in this thread.
Personally I'm not against classes, if someone prefers to use classes instead of prototype it's fine by me.

What I don't understand is why the TC39 is pushing for classes and classic OOP instead of other more pressing issues. You can accomplish OOP with prototypes.

IMO the lack of type checking is much more problematic. Flow and TypeScript are just adding a new layer of problems to an already convoluted workflow.

You realise JS classes are syntactic sugar for prototypes, right?
That's precisely my point.

Classes shouldn't be a priority.

There's a cute use of WeakMap to do private fields in a GC-friendly way

    let ages = new WeakMap()

    export class Person {
      constructor(name, age) {
        this.name = name
        ages.set(this, age)
      }

      // ages.get(this)
    }
Wouldn't it be funny if we see a resurgence of compile-to-js languages? If JavaScript keeps on piling up layers I could see people longing for a simpler, smaller language.

Where there was coffeescript to highlight functional features in JS in a time where most JS was imperative, maybe we'll see a language that takes the OO sugar away to, again, reveal the functional language that's underneath.

FWIW the arguably ideal target for compile-to-web tech, WebAssembly, is adding some OO too [0].

0 - https://github.com/WebAssembly/gc/blob/master/proposals/gc/O...

Until wasm has GC, we're not likely to see much in the way of compile-to-web outside of C, C++, or Rust.
There is already a .NET prototype.

https://github.com/SteveSanderson/Blazor

https://www.youtube.com/watch?v=MiLAE6HMr10

WebAssembly without native GC support is no different that targeting a real hardware CPU. One just has to implement it as well.

As soon as WebAssembly reaches a more mature state, expect the resurgence of plugins and this time around we won't be able to disable them.

blazor is not .NET compiled to WASM. blazor uses a .NET runtime called DotNetAnywhere, which is written in C and compiled with emscripten. the actual .NET IL is then interpreted at runtime by DNA.
True, but the fact is that people are already toying with the idea of bring such runtimes back into the browser.

Also if you bother to read the meeting minutes from WebAssembly meetings, developers from .NET team are present in such meetings.

I am absolutely planning on forking an old school JavaScript off of the current runtimes. Or at least forking NPM. Not soon, because for now writing ES5 and running it on ES6 runtimes is Good Enough for my current purposes, as I'm still <1.0.0 so nothing really matters. But eventually I'm going to want a separate package space, and a separate language community.

Modern JavaScript is cool, but it loses many of the benefits of old school JS:

- only runs on newer devices

- requires a precompile step

- more language to learn

- confusing concurrency model

- confusing inheritance mechanisms

- no incentive to learn to use closures, callbacks, and prototypes properly

If I was willing to accept all of those things, I would just use a straight up better language, like Rust or Haskell, and get the additional benefits of type checking and deterministic performance.

But I'm not interested in those things. I want a simple run-everywhere language for beginners to get started on. JavaScript used to be that, but modern JavaScript is not that language anymore.

So, I'm going to bring back ES5. Why not. Who's with me?

I remember when Crockford released the fantastic book, JavaScript The Good Parts. I disagreed with some of his opinions in that book, like avoiding the use of 'new', but that book - more than almost anything else, helped me educate folks on how to use js well. I used to order copies for every member of my team. I wonder if, instead of a fork, we can just make a case for a subset of js. Take the good new things, and ignore the rest, like a modern "Good Parts".
Could you elaborate on the things you disagreed with? For example, for some reason I prefer avoiding new but I'm conflicted about it. I'd really like to hear your input as to why perhaps using 'new' is a good thing.
Not OP, but new is dangerous because it makes it easy to put a lot of methods on an object, which is generally an anti-pattern.

But sometimes that's what you want. I think a good use case is view models... You're not mutating anything, but you have a large number of consumers that are using different subsets of views on a piece of data. Constructing those by hand can be a PITA.

Although, it's dangerous there. "Lots of views on a piece of data" might be a way of covering up "several disparate pieces of data in one place" or "data that is playing two roles when it should be transmuted instead". So, it's good to be afraid of new.

I am still frequently decomposing a single index of objects into separate indexes of literals fairly often. I often get sucked into thinking something is an object when it's really just a few separate pieces of data indexed together.

I also use new for libraries that export something that isn't exactly a function, but more a point of reference. For example my browser-bridge module is a point of reference for the computation between a web request and response. It's not really an action, but it's a thin representation over a handful of low level things you want to do in that space. It's purpose is to wrap up a set of concerns.

Essentially, OO is generally bad in JavaScript, but sometimes it's good and in those cases new is nice.

There are a lot of reasons, honestly, and it's great beer conversation, but from a high level: producing a concrete instance that invokes a constructor function with a shiny pristine 'this' to build on is a pretty great pattern. Add the ability to muck about with clever prototype manipulation, and you have pretty enormous power - you can do everything that classic OOP can do (if that's your thing) and a lot more.
I always wonder why if people need these features so badly they don't just program in Java or C++?
Because currently they aren't supported on the browser.

But thanks to WebAssembly they will be eventually there again.

I find it extremely amusing that the once clear JavaScript turns into ugly Perl, whilst Perl thrives to clear its ugly sigils and starts using ascii keywords for such properties. Design by committee rears its ugly heads