-
Notifications
You must be signed in to change notification settings - Fork 113
Usage of private member inside computed properties #263
Comments
Isn't that construction bad from the outset? It almost appears as if you want Given that, I agree. Unless that context contains a private field |
Yes, it is a very bad construction and I can't see any case where such construction would execute without throwing an error (maybe I'm wrong here). But since it is a valid JS program, we need to have this covered with tests =). The major problem of current spec is that it is not intuitive to figure out why |
It would work in a scenario like this: class B {
#f = "foo";
C = class C {
[this.#f] = "Test262"
};
} Unless I'm misunderstanding something, Given how private fields have been described, it's somewhat intuitive why you'd get a |
FWIW, with the current design of implementation in V8 I believe a TypeError would introduce less overhead than ReferenceError. With the example in the OP:
If it's a TypeError, V8 can just pre-initialize all the private names (I believe that's also what the spec change would look like) so that it only needs to check that the object does not contain that private field at runtime. If it's a ReferenceError, currently V8 needs to insert a hole check for every access to Refs: https://bugs.chromium.org/p/v8/issues/detail?id=9611 This overhead does not seem to only apply to V8, @caitp and @caiolima would know more about what this entails in JSC. But I think it's reasonable to assume that differentiating the error types based on the order of the access and declaration comes with a cost in implementations. |
I’ve said before that it would be a bit easier/faster to emit a single error for “property does not exist” if the private name isn’t present on an object. Otherwise, we need to pre-define all fields on an object in the TDZ, and emit distinct errors if the field is accessed in the TDZ or not defined at all. But I do acknowledge the user gets a slightly better experience (error message) with the current design This might look like new class {
[this.#f] = ‘test262’; // TypeError: invalid private field #f
#f = ‘foo’;
} as opposed to a ReferenceError indicating #f is still TDZ’d |
I’m confused why this is a problem; referencing something that doesn’t exist should be a reference error and using a field on the wrong type of object should be a type error. |
it's not a problem, it's just more complicated (and a little more expensive) than doing it differently, for arguably little gain (how little is debatable, as some value good error messages higher than others). |
I think consistency with the rest of the language is pretty valuable. |
In my specific case, this was very confusing because I had the mental model where classes declarations are not affected by ordering. However, I don't think it can be used as a strong argument, since my mental model is just wrong. The problem is in the fact that it takes quite some effort to understand why it is a) throws |
I don't think this is really consistency with the rest of the language, since private fields aren't really comparable to lexical variables. Ordinarily, if you access a property which hasn't yet been defined, but eventually will be, you get "undefined". Where do we ever throw a ReferenceError in this case, except when the base object is TDZ'd? |
This is akin to const and let, where accessing it prior to its lexical declaration throws a reference error. Ordering of class fields absolutely matters, and was a major debate point during the advancement of the proposal - consider |
This logic doesn't apply to non-private fields, so I don't really agree
If we were going to make the argument that class fields behave like lexical variables, we'd have some consistency across private and non-private there, IMHO. Really, I think private fields are their own thing, distinct from lexical variables and public fields, and we have a choice of whether to throw a ReferenceError and make implementations a bit more complicated, or go for simplicity (with a minor usability cost) |
The problem here is related with when we initialize the binding of a |
@ljharb I get it now. Thanks for clarifying. |
Consider: new (class Outer {
f = 'outer';
constructor() {
return new (class Inner {
[this.f] = 'Test262';
f = 'inner';
});
}
});
// { outer: 'Test262', f: 'inner' } In this example, new (class Outer {
#f = 'outer';
constructor() {
return new (class Inner {
[this.#f] = 'Test262';
#f = 'inner';
});
}
});
// { outer: 'Test262', #f: 'inner' }
// Equivalent
new (class Outer {
#f = 'outer';
constructor() {
const key = this.#f;
return new (class Inner {
[key] = 'Test262';
#f = 'inner';
});
}
}); With that change, both of OP's examples should be throwing This conveniently matches Babel's implementation. |
I have been wondering whether it is actually possible to create a valid reference to a private field in the computed property key in the same class. e.g.
Since you can't reference |
The ordering here is observable and doesn’t require knowing the spec. I think it’s very instructive for users to see a ReferenceError and get a hint that something different is happening - ie, it can help them form their mental model about what “this” means in a computed property key. |
I don't agree that to understand the difference between those errors, the user don't need to know the spec. At least to me, it was not very intuitive and I couldn't find any place where we state something like |
The way i think about it is, the binding of everything happens in the order it’s declared, and everything nonlegacy is in tdz until it’s lexically initialized. |
I'm not sure it's a "solution" so much as a just "the actual way this is spec'd" --- since computed property names are evaluated during class evaluation, not during instantiation. I think we've been using them just to illustrate the ordering and it isn't really central to the point that's been discussed in this thread (but, maybe I'm missing something?) |
The point I’m making is that if we change current PrivateName resolution and use outer scope instead of current class scope, there is no ReferenceError or TypeError when using private members inside computed property on a) and b). In the case of the absence of the PrivateName into the outer scope, it would then be a SyntaxError, since we are using a private name that is not defined. That’s why I think it is an alternative to solve this issue, since it wouldn’t exist anymore. |
It sidesteps the issue observable by using computed property keys by making it always fail the same regardless of the fields' order.
I think what you're discussing here can be demonstrated with initializer ordering: class Ex {
#foo = 1;
#bar = this.#foo;
}
// Vs
class Ex {
#bar = this.#foo;
#foo = 1;
} Which is then directly translatable to block scoping: {
const foo = 1;
const bar = foo;
}
// Vs
{
const bar = foo;
const foo = 1;
} Whether this translates to a |
I think you just missed one of the points @caitp is trying to make: In: class Ex {
#foo = 'bar';
[this.#foo] = 1;
}
On the other hand, with class Ex {
#foo = 1;
#bar = this.#foo;
}
|
(only talking about your first example) In the case where |
First, I'm not a fan of referring to the outer private name scope (#263 (comment)) for roughly the same reasons as @ljharb stated. It is true, as @caitp says, that private fields are not lexical bindings. However, that To me there is the separate question of whether it is reasonable to expect the runtime semantics (to wit, TDZ) of "lexical things" in the language to extend to |
@ljharb (only talking about your first example) In the case where There's only one problem I have with your assessment: the fact that you even consider class Outer {
#foo = "outerField";
Inner = class Inner {
#foo = "innerField";
[this.#foo] = "bar"; //TypeError
}
} The reason is that if It get's even more curious though. According to everything that's been pushed about private fields, Long story short, there is no circumstance for which the |
Once an accessor is declared, later access to it in computed property keys should throw a TypeError (for brand checks) instead of a ReferenceError. This patch implements it by building the AccessorPair eagerly once *any* of the component is visited so that the hole check can be passed, and building the private class brand before visiting class member keys so that the brand can be checked in computed property keys. This means that we cannot create the complementary getter and setter together, and have to split the creation of the AccessorPair and the assignment of at least one of its component, in case one component is accessed before the other one is declared: class C { get #c() { } [this.#c] = 1; // TypeError set #c(val) { } } Note that this is still a ReferenceError: class C { [this.#c] = 1; // ReferenceError get #c() { } set #c(val) { } } Refs: tc39/proposal-class-fields#263 Bug: v8:8330
Thanks for filing this issue. I like to explain private fields and methods as, "the name can be accessed only inside the curly braces for the class body". This means, logically, that we should hoist the definition of the private name to the beginning of when the class scope is created, rather than when the method or field declaration is reached. It was simply a drafting error of mine to not do this hoisting earlier. Think about what the temporal dead zone is for: TDZ gives us a useful error when a binding does not yet logically have a value. But private names always have a logical value when they are in scope, since we are lexically inside the class. Methods are logically added to the class "all at once, at the beginning", rather than one by one. So it's pretty surprising if the private names are defined one by one, based on the linear execution of the class. Even for private instance fields, it's the same name for all instances, so it also makes sense for the name to be defined "at the beginning". The ease of implementation is a nice side-benefit, but it's not the motivation for this fix. |
Private names are logically defined during the whole class execution, not only after the definition is reached. This patch properly hoists the private name definition, to avoid an unintentional and confusing TDZ condition. Closes #263
@littledan Just for clarification: If so, this still cannot work. While the private name will indeed be available to the class, there is still no |
@rdking You're right that there will be no such instance. So we will get a |
@littledan Not only is it not interesting, it's not useful. That's my point. If But that just goes back to what I was saying about there being no possible way for a private field in any class to have an accessible value during that same class's definition evaluation. The private name may be defined, but no value would exist anywhere at that time. This is where we get back to the OP. Since there is no circumstance (not even with the new PR) under which it would be possible to retrieve the value of a non-existent private field for use in the calculated name of a sibling field, there's only 1 possible error instead of 2. Considering the validity of a lexically available private name in this scenario is pointless. As such, the only place where it make sense to consider the validity of any private name is within the scope of a lexically declared method of the class. So the PR did not change anything useful... unless I missed something. |
I don't think we will change the scope to each of the methods and initializers, rather than the whole class body. This would be much more complicated. I agree with you that it's not useful in a computed property name, but we still need to define how the error gets there, and which error it is. |
I'm not asking that you do this either. That would limit what future proposal addon's to this are capable of. I'm merely speaking to the effective limit of the proposal as it currently exists. Even the PR doesn't change that limit.
How the error gets there is pretty straight forward. I think I've explained it twice already. All that's really needed is for you to decide on what kind of error it is. From where I stand, it looks like a Given cases a) and b) from the OP, the PR eliminates case a), but still causes a Side Note: Unless I'm mistaken, the PR will suddenly become very handy if some means of keyword-based class self-referencing becomes available in the future. |
Sounds like you are in favor of this throwing a TypeError. Me too. You can find an explanation of why the current spec actually throws a ReferenceError upthread. So it seems like we are all in agreement that we should fix this issue. |
Recap of discussion with @jridgewell for some illustrative examples of TDZ in class scopes { let C = 1; C = class C { [C](){} } } This is a TDZ error because { let C = 1; C = class C extends C {} } This is a TDZ error because the heritage expression is oddly enclosed, lexical scope wise, under the |
@syg Close, but not a fit. Remember that everything on the right side of the So even while it seem's like a similar case, the semantics involved are very different. |
@rdking that’s a block, not a class. There’s no field in that example. |
There aren't fields in that example. Sorry those aren't really examples of this particular discussion with fields, but instead illustrative TDZ examples that @jridgewell requested be documented in the thread.
@rdking That's not what's going on. The |
Ok. So I learned something new. The name of the class is a member of the declarative lexical scope of the class being declared. Seems like an odd choice, but oh well. In either case, the result is the same, the definition isn't yet complete so |
Last week I noticed that current spec is throwing different types of Error depending on ordering of declaration and usage of private members. While I understand the reason behind both evaluations, I'm not convinced they are very intuitive. See examples below:
a)
and
b)
The difference is: in
a)
, ThePrivateIdentifier
#f
is not initialized yet and we fail when trying to get the binding of an unitialized id. inb)
, the id is already initialized and we fail because there is no such private field intothis
. It would be simpler (to understand and to implement) if we throw the same error in both cases.The text was updated successfully, but these errors were encountered: