proposals
proposals copied to clipboard
Class Fields (Stage 3)
Original issue submitted by @babel-bot in https://github.com/babel/babel/issues/4408
Champions: @jeffmo (public class fields) @littledan (private + combined) Spec Repo: https://github.com/tc39/proposal-class-fields Spec Text: https://tc39.github.io/proposal-class-fields/ Slides: https://drive.google.com/file/d/0B-TAClBGyqSxWHpyYmg2UnRHc28/view
Moved to Stage 3 at the July 2017 meeting: https://github.com/tc39/agendas/blob/master/2017/07.md (https://twitter.com/lbljeffmo/status/890679542007738368)
Examples
class C {
static x = 1, #y, [a];
z, #w = 2, [b];
a() {
this.z;
this.#w++;
#w; // #w is this.#w
}
}
Parsing/ESTree
- [x] AST handled already in current
classPropertiesandclassPrivateProperties. - [ ] May need to rename though (new plugin name).
- [x] make sure no "private computed" parse
- [ ] need to support comma separated properties.
Transform
- [ ] combine the class-properties plugin + tbd private properties one
Contacts
- #proposal-class-fields on Slack!
- @littledan
- @diervo (dval on Slack)
- @Qantas94Heavy
dunno how much of an help this is, but I've been experimenting already with this and my conclusion is that, to represent the current proposal, each class needs it's own WeakMap.
Example
source
class A {
#value = 1;
valueOf() {
return this.#value;
}
}
class B extends A {
#value = 2;
toString() {
return String(this.#value);
}
}
pseudo target
const privatesA = new WeakMap;
function A() {
privatesA.set(this, {__proto__: null, value: 1});
}
Object.defineProperties(
A.prototype,
{
valueOf: {
configurable: true,
writable: true,
value: function () {
return privatesA.get(this).value;
}
}
}
);
const privatesB = new WeakMap;
function B() {
A.call(this);
privatesB.set(this, {__proto__: null, value: 2});
}
Object.defineProperties(
Object.setPrototypeOf(
Object.setPrototypeOf(B, A).prototype,
A.prototype
),
{
constructor: {
configurable: true,
writable: true,
value: B
},
toString: {
configurable: true,
writable: true,
value: function () {
return String(privatesB.get(this).value);
}
}
}
);
in that way new B().valueOf() would be 1 and new B().toString() would be "2" + no internal property ever leaks through symbols.
Isn't that only the case if "value" is transformed to a string? I'd think you could use an in-scope constant - like a symbol or an object literal - and then it'd be unique and you could share one WeakMap for the entire file.
I'm not sure I am following you ... the example has nothing to do with the toString or valueOf case, it's a proof of concept of the implementation details and nothing else: WeakMap is the answer to this problem. It scales, it's not the best for performance reasons, yet is the most reliable.
I understand that you must use a WeakMap. I'm responding to "each class needs it's own WeakMap" - it seems like both class A and B could share the same WeakMap, since each "value" is a distinct PrivateName.
A single WeakMap could be used as @ljharb says, but I think using a WeakMap for each class simplify the implementation.
It's not just a matter of implementation, it's a matter of standard. You cannot have shared WM between classes because private field #a accessed via an inherited method is NOT the same private field #a accessed by subclass method.
Please understand the current proposal before premature optimizations
It might be better to discuss this on the implementation PR than on this thread. I like the implementation strategy of that patch: In spec-compliant mode, there's one WeakMap per field, and in loose mode, there are no WeakMaps, since it's all based on string properties. It's important to track which instances have each individual field, since an initializer can throw an exception and leak an object that has some fields but not others.
@littledan agreed, but I couldn't literally find related changes. If there's already a PR I'll have a look there, thanks
New private props PR https://github.com/babel/babel/pull/6120
Replaced with https://github.com/babel/babel/pull/7555
Private fields shipped in https://github.com/babel/babel/releases/tag/v7.0.0-beta.48
TC39 is looking for feedback from the committee, since unlike public fields which has extensive usage/docs/videos/etc, private has not (even as Stage 3) since it hasn't shipped in Babel or other implementations until recently.
The following seems to be allowed by the Babel parser:
class Foo {
# p = 0;
constructor() {
this.# p ++;
}
print() {
console.log(this . # p);
}
}
However, it seems to violate the lexical grammar in the spec proposal:
PrivateName:: #IdentifierName
Is the intention that the transformation itself (not the parser) should disallow this.# foo and similar productions?
cc @ramin25 who helped find this, and @rricard, who is working on private fields.
I can try to take a look when I'm done with static private fields (I'm getting there, slowly but I'm getting there, and @tim-mc helps me as well on this one). This issue though seems to be a parser-level issue, I never worked directly on that and only consumed the AST but that might be an interesting thing to try to fix.
It should be disallowed by the parser :+1:
@mheiber @rricard @ramin25 Thanks for your helpful observation; I agree with the analysis. We specifically considered and rejected permitting whitespace here. I look forward to the fix here.
Hello you guys, I'm a little late to the conversation, but I've made a class implementation that has public, protected, and private fields: lowclass
(And it really works! See the extensive tests, for example extending builtins like Array).
With the ability to extend builtin classes, and truly protected and private class properties, it has everything needed to support transpiling classes with class fields.
(Plus, using it as a lib gives you even more features like "module protected" fields, and other tricks.)
I use it to create classes that work with native and polyfilled Custom Elements, for example the test just works.
My implementation organizes WeakMaps under the hood, and it works with async code (unlike some other implementations that rely on synchronous call-stack tracking).
Thanks to @WebReflection's babel-plugin-transform-builtin-classes, @Mr0grog's newless, and @philipwalton's mozart for the inspiration and knowledge needed for creating my implementation.
So, I had a thought:
What if instead of compiling to unreadable machine code, we compile to code that is much more readable?
Example:
class A {
#value = 1;
valueOf() {
return this.#value;
}
}
class B extends A {
#value = 2;
toString() {
return String(this.#value);
}
}
would transpile to
// ES5 output!
var Class = require('lowclass')
var A = Class('A', function(accessHelpers) {
var Private = accessHelpers.Private
return {
private: {
value: 1
},
valueOf: function() {
return Private(this).value;
}
}
});
var B = Class('B').extends(A, function(accessHelpers) {
var Private = accessHelpers.Private
return {
private: {
value: 2,
},
toString: function() {
return String(Private(this).value);
}
}
});
Oh, I forgot to mention, it also works with super and get/set, with shorter syntax using arrow functions and concise methods in newer environments, f.e.,
var Class = require('lowclass')
var A = Class('A', ({Private}) => ({
private: { value: 1, },
get value() {
return Private(this).value;
}
}));
var B = Class('B').extends(A, {
toString() {
return String( super.value ); // super!
}
});
but in older environments, it is still possible to use the Super helper with the accessors:
// A.js
var Class = require('lowclass')
var Private
var A = Class('A', function(accessHelpers) {
Private = accessHelpers.Private
return {
private: { value: 1, },
}
});
Object.defineProperty(A.prototype, 'value', {
get: function() {
return Private(this).value;
}
})
module.exports = A
// B.js
var Class = require('lowclass')
var A = require('./A')
var B = Class('B').extends(A, function(accessHelpers) {
var Super = accessHelpers.Super
return {
toString: function() {
return String(Super(this).value);
}
}
}));
TLDR, it can do everything, in ES5.
(I haven't tested those samples, there could be some typo)
That sounds like a transform you could certainly build, but making compiler output human-readable at the cost of needing an additional runtime library doesn’t seem like a worthy tradeoff.
Maybe some parts or ideas could be integrated? I think the specific code for ES5 could be removed and left only the private and protected parts, and left Babel to adapt it itself...
By the way @trusktr, can you provide an example of protected attributes and methods? Examples seems to show only private ones... And are protecte attributes in any standard?
additional runtime library
@ljharb If lowclass (or similar idea) gets adopted into Babel, then it won't be "additional". :D
an example of protected attributes and methods?
@piranna Sure, there's many examples in the tests. For example, search for "protected" in this file.
Small example:
// Parent.js
import Class from 'lowclass'
export default Class('Parent', {
protected: {
parentLog() {
console.log( 'Parent!' )
}
}
})
// Child.js
import Parent from './Parent'
export default Parent.subclass('Child', ({Protected}) => ({
childLog() {
Protected(this).parentLog()
console.log( 'Child!' )
},
}))
// app.js
import Child from './Child'
const o = new Child
o.childLog() // it works
// output:
// Parent!
// Child!
o.parentLog() // ERROR, undefined is not a function (because it is not public)
- The rule for lowclass "protected" properties is they are accessible in any subclass (and by super classes), but not public. It's similar to C++ and other languages.
- "Private" properties are only accessible by the owning class (similar to C++ and other languages).
- Anyone can access public.
- The access helpers can be leaked out of the class (as in one of my above examples) to do things similar to "package protected" in Java. (Examples in the README)
- private and protected fields can be shadowed by subclasses (unlike other languages that instead throw a compile error), so this is inline with this class fields proposal.
- besides shadowing, lowclass also offers "private inheritance"
@trusktr I think the current style of output from Babel is good. I don't think "readable" output is a sustainable goal for Babel transforms for TC39 proposals, given that the language semantics are pretty subtle. If you want to propose particular semantics, I think that's better done in TC39 than in this repository; see CONTRIBUTING.md.
I also don’t think it would be appropriate for Babel to contain any concept of “protected”, because the language has none - and it in my opinion is not likely to ever have one.
@littledan Readability would only be a bonus. I'm just showing a runtime implementation that has all the features needed for everything in this proposal (and it can be tweaked). Maybe it'll can give someone ideas.
don’t think it would be appropriate for Babel to contain any concept of “protected”, because the language has none
Not sure what you mean. JS has a protected reserved keyword, just like private. How is it more appropriate for Babel to contain a "private" implementation than it is for "protected"? Is it because "private" is spec'd in the class fields proposal, and "protected" is not?
We can easily add it.
When I write classes, a large amount of the time I want "protected" (and I use lowclass for it), especially when designing an API that someone will interact with that is composed of a class hierarchy where I don't want the end user to reach into protected parts of instances, but I do want my subclasses to access protected parts. I like that I can guarantee a certain public interface to end users of my instances while keeping protected properties hidden.
I made a poll to see if people want protected (I hope some people will vote!): https://twitter.com/trusktr/status/1025466478626136064
By the way, it's possible to add encapsulation with lowclass around existing classes. (also in the README)
This shows that such an implementation can exist on top of Babel's class transform output as is:
import protect from 'lowclass' // call it "protect" instead of "Class"
const Thing = protect( ({ Private }) => {
return class Thing {
constructor() {
// make the property truly private
Private(this).privateProperty = "yoohoo"
}
someMethod() {
console.log('Private value is:', Private(this).privateProperty)
}
}
})
const t = new Thing
t.someMethod() // "Private value is: yoohoo"
The class definer function can return any class, for example one created with Babel helpers.
Something like a babel-plugin-transform-class-fields could easily wrap the output from @babel/plugin-transform-classes. I think I'd like to give it a shot! It'd be nice to have class + protected.
@trusktr yes, and the private fields proposal intentionally and explicitly chose not to address "protected", and https://github.com/tc39/proposal-private-fields/blob/e704f531c33795ca34ede86e1c78e87798e00064/DECORATORS.md#protected-style-state-through-decorators-on-private-state shows how you can achieve protected without special syntax for it.
I found a couple issues with the current implementation: https://github.com/babel/babel/issues/8421.
Interestingly, the problems described there are the same as in other languages.
That version of "protected" is exactly the same as implementing "private" with a weakmap, then assigning the WeakMap onto each instance. Not very protected.
There's valid cases for "protected" where protected members can not be public: An instance factory can provide instances, while the class hierarchy is encapsulated. Therefore, all the "protected" members are still hidden by design, and library maintainers can benefit from the protected pattern.
Simply making everything public (like in the decorator example) isn't the same.
Since JS has runtime subclassing, anything that's "protected" is in practice fully public.
Oh yeah, I almost forgot about access to the prototype. A way to defend against that would be to have a library's leaf-most classes define a private property, leak the access helper inside the module scope (f.e. with lowclass), and let library code check private values by reference to prevent duck-typing of any sort.