-
Notifications
You must be signed in to change notification settings - Fork 10
Class brand #13
Comments
There are indeed techniques to install private fields onto arbitrary objects via a combo of super and return override. While It seems like this suggestion would require every class to contain an implicit private field, that's not accessible to user code, that your new mechanism would test. That could have performance implications for all existing classes. Another challenge to totally new syntax is that the list of available reserved words is quite small, and there's not much that would be appropriate. |
Objects which get private fields installed using said technique effectively become instances of the class returning the objects in the constructor. Is there the goal for your proposal to work cross-realms? That's something unexpected, imo, for instances of custom classes (where private fields can be declared). If there isn't already some kind of per instance internal custom branding mechanism, required to implement other existing class functionality, an alternative to not bring that cost to all classes could be to make it opt-in. There could be some additional cost to implement it for classes having no private fields, though. One way to maybe have this branding using existing mechanisms and no additional costs would be for instances of these opting-in classes to have their prototype be immutable. The prototype object itself would have its Example: stable class Foo {
}
// Somewhere
if (o instanceof Foo) {
} The problem would be shifted into providing more guarantees to objects created using classes. Possibly, even, the machinery achieving this could be provided as utilities, so that the same guarantees could be achieved by poor-man constructor functions, not using the class statement. |
Certainly However, forcing a class to adopt observable changes in order to be checked in this manner violates encapsulation - it makes "adding brand checking" a breaking change. A critical ability with private fields is the ability to make adding and removing them on a class be entirely unobservable (by adding a guard around the field access). |
I don't get it. As I understand, "adding brand checking" is always a breaking change.
What "adding and "removing private fields on a class" mean? If a private fields was installed on an object u can't remove it. So I guess u mean in the next version of your code u remove/add private field, but in almost all cases (refactoring public fields to private fields or something like that), it will be breaking change. |
Adding unguarded brand checking is a breaking change. With a guard (try/catch, or this proposal), it can be made to not be a breaking change. Yes, I'm talking about across time - so I can avoid a semver-major bump. Certainly removing a public property is unavoidably breaking, but we're not talking about public fields here :-) |
So what's the key difference between |
The former is the user opting in to checking an implementation detail they created. The latter is automatically opting all classes and instances into carrying around additional hidden internal state, on the possibility someone will apply this I'm not saying that this kind of general "is class" check is bad - I'd love it to exist - but it has its own problems, and simply wouldn't address the ergonomics. I have code that's accessing |
I am not sure how these two have significant difference in practice. Or maybe I'd like to say "opting in to checking an implementation detail they created" seems not common mental model developers have... It seem only apply to spec writers's checking internal slot cases. As my experiences, js programmers only use I think we need some real use cases to check what programmers expect. In my current impression, |
In my experience, |
All public apis can't control the source of the objects passed to them, but I don't think they use |
That code is often written to be brittle does not lessen our responsibility to provide mechanisms for writing code robustly. |
I agree. :) But let's go back to |
With private declarations being able to be shared among objects (for example with the private declaration proposal), asking if something matches the private fields of a specific class is not granular enough. You need it on a per-field basis. |
Yes, you're right. However, it just seems like a perfectly acceptable compromise for a new, much needed and powerful capability — safe class testing in JavaScript. |
Precisely. |
@devsnek
It's interesting to see typing as something which could be reduced to the most granular level, the property. Friend classes need access to several of each other's members. Would we export all private declarations to all friend classes? Given that with private properties we've effectively left the Symbol, prototype and Hash Table interpretation of objects, I believe it would be better to just go the Type way. Solution A
Solution B I would almost certainly never use private properties on ad hoc "untyped", plain (HashTable) objects. |
@devsnek I am not sure I understand what you mean. I think code could help. Could u give a small example? Thank you very much! |
@dcleao in JS, a "friend class" isn't really a thing that exists. With private declarations, two friend classes would be ones created in the same lexical scope as the declaration. That you might never use that doesn't mean it's a) bad or b) others won't. I'd use it constantly, and I think it's quite a good pattern. |
var someGlobal;
class C() {
constructor() {
someGlobal = this;
throw null;
}
}
class D extends C {
#field = 42;
constructor() {
super();
}
hasField() {
return #field in this;
}
}
try { new D(); } catch { }
someGlobal instanceof D // true
someGlobal.constructor === D // true
someGlobal.hasField() // false
someGlobal of class D // ?? In this example, what is However, what about this example? var someGlobal;
class C() {
constructor() {
someGlobal = this;
}
}
class D extends C {
#field = 42;
constructor() {
super();
throw null;
}
hasField() {
return #field in this;
}
}
try { new D(); } catch { }
someGlobal instanceof D // true
someGlobal.constructor === D // true
someGlobal.hasField() // true
someGlobal of class D // false In this case, the previous definition of In other words, while I think that a generic proposal for this is awesome, I think it simply does not replace or subsume this proposal's featureset, because of currently allowed edge cases. Given other delegate feedback from today's TC39 incubator call, I'm going to close this (but i encourage you to pursue this in a different proposal). |
I disagree. The semantic only depend on how we define the time to install the brand. Note this is only the edge case (which intentionally create a partial object --- a very bad thing in real OOP), I really don't like use such impractical edge case to differentiate two possible solution. |
If the full constructor for The intention of this proposal is to detect if a specific field is present. The proposal mentioned in this issue simply doesn't - and can not possibly - satisfy that for all cases. |
It’s also worth noting that even if one of the private fields is initialized, it is still possible that not all of them are. class Foo extends function(o) { return o; } {
#first = 1;
#second = (() => { throw null })();
static hasFirst = o => checkHas(() => o.#first);
static hasSecond = o => checkHas(() => o.#second);
}
let checkHas = fn => {
try {
return fn(), true;
} catch {
return false;
}
};
let obj = {};
try { new Foo(obj); } catch {}
console.log('obj.#first exists', Foo.hasFirst(obj)); // true
console.log('obj.#second exists', Foo.hasSecond(obj)); // false It’s not just unsuccessful construction where this can come up, either. Private fields can be manually reified and shared with things outside the class scope where they were declared — class scope only constrains their syntactic availability, but you can create two classes (or even arbitrary object factories) that initialize the same private field. Example of manual reification w/ slot-style TypeError messagesclass PrivateField {
#add; #get; #set;
constructor(name) {
let capability = PrivateField.#createCapability(`${ name }`);
this.#get = capability.get;
this.#set = capability.set;
this.#add = capability.add;
}
get(o) {
return PrivateField.#access(this.#get, o);
}
has(o) {
try {
return this.#get(o), true;
} catch {
return false;
}
}
initialize(o, v) {
return PrivateField.#access(this.#add, o), this.#set(o, v), o;
}
set(o, v) {
return PrivateField.#access(this.#set, o, v);
}
static #access(fn, o, v) {
try {
return fn(o, v);
} catch {
throw new TypeError('Illegal invocation');
}
}
static #createCapability(name) {
// Doesn’t have to be eval to work; this is just to let it
// show up with a specific name in devtools.
return eval(
`(class $ extends function(o){return o}{` +
`#${ name };` +
`constructor(o){super(o)}` +
`static add=o=>void new $(o);` +
`static get=o=>o.#${ name };` +
`static set=(o,v)=>o.#${ name }=v` +
`})`
);
}
}
let $eggCount = new PrivateField('eggCount');
let example = { get eggCount() { return $eggCount.get(this); } };
$eggCount.initialize(example, 12);
console.log(example, example.eggCount); |
This is only a definition issue, not a practical issue. Actually in other OOP languages like Java or C#, u can also throw error in constructor to and the |
I think it’s both - if we’re going to add new syntax solely for determining if something is a “real” instance of a class, it’d have to actually meet that definition, or else it would be confusing in practice. |
@ljharb It's not a black/white problem. In other OOP languages, there is also similar dark side (throw before constructor/initializer finished, is that object a "real" instance?). Eventually there will be a definition for such edge cases. So the only important point is whether "class brand"-style solution could achieve the same behavior to "duck type"-style solution. |
That's something that I think is great to explore in a separate "class brand" proposal - but it still doesn't completely solve the problem this proposal solve, so it wouldn't make sense to block this one on it. |
If private fields are specific of classes (are they not?), there is no case where you'd want to check for the existance of a particular private field which could not be satisfied by testing for a given value being an instance of the current class.
While
instanceof
can be spoofed, and might not be a 100% safe option because of that, a new, unspoofable class brand check could be devised, whose uselfuness would go beyond testing the existance of a private field.Example:
Or, can the class of an object which is an instance of an "actual" JS class be spoofed as well (via
constructor
)?The text was updated successfully, but these errors were encountered: