-
Notifications
You must be signed in to change notification settings - Fork 25
[[Set]] vs [[DefineOwnProperty]] #42
Comments
Ping @ljharb; do you see any I missed? |
When talking about a base |
Here's another, slightly more complex argument in favor of Define: Because static methods and static fields are installed as properties of the same object, I would expect them to work very similarly. It would be very surprising if, for example, class A extends B {
static set foo(){}
static foo = function(){};
} triggered the setter, given that class A extends B {
static set foo(){}
static foo(){}
} redefines So (if you buy this argument) static fields ought to be created with DefineOwnProperty, presumably as writable, configurable, and non-enumerable. For consistency, non-static properties should be created the same way. |
Interestingly, Babel here doesn't actually redefine it, although Chrome native does. |
Babel gets class properties wrong in a number of ways - for example. The spec is unambiguous (see step 21.b.i and here); Babel's just wrong. |
I don't find that argument compelling. Consider the following code, which I claim is completely analogous: var o = {
set foo() {}
};
o.foo = function () { }; vs. var o = {
set foo() {}
foo() { }
}; It is very unsurprising to me that the first triggers the setter and the second redefines. |
@domenic, I would think the closer analogy would be var o = {
set foo(x){},
foo: function(){}
} given that the field is inline, not after-the-fact. |
Nah, that uses |
Eh, part of the discussion is over which sigil to use. Without considering the sigil, it seems that static field declarations ought to work the same as static method declarations. |
(Although even with class A {
static set foo(x){}
static foo = function(){}
} is more visually similar to var o = {
set foo(x){},
foo : function(){}
}; than var o = {
set foo(x){}
};
o.foo = function(){} to my eye.) |
A subsequent duplicate declaration without an initializer isn't something we have to cook up never-written object literals to think about. JS already has this: var x = 0;
var x;
console.log(x); // Does not print 'undefined' The cognitive burden here just seems enormous for no actual upside. How would you explain this feature to a user? Set
Accurate & is analogous to a thing people actually do today. Define, v1
Sugar for a thing I never do? What is Define, v2
Is analogous to a thing people actually do today & spells out the consequences of using this feature when you're not in a position to inspect the implementation of the base class. |
To be clear, the double declaration was just to make the examples shorter; I should have written them out more. The code you'd actually see today would be more like class Base {
static set foo(x) {}
}
class Derived extends Base {
static foo() {} // does not trigger setter
} or class Base {
set foo(x) {}
}
class Derived extends base {
foo() {} // does not trigger setter
} or let proto = {
set foo(x) {}
};
let object = {
__proto__: proto,
foo: 0, // does not trigger setter
};
Personally? "It works like an object literal." The point of classes is to be declarative, after all. |
I don't understand the value of analogizing to code that no sane person writes. Re your other example, consider
vs
What happened to "new syntax should do new things" here?
Object literals don't have inheritance, so we're back to where we started in terms of describing behavior in terms of existing primitive operations. The closest thing is |
Those do meaningfully different things already, as observable in the widespread if debatably advisable practice of using arrows in public fields to get "auto-bound" methods.
... Yes they do? Even without |
You can't write |
const obj = {
__proto__: { set x(v) { console.log('not called'); } },
x: 3,
} or Object.defineProperty(Object.prototype, 'x', {
set: v => { console.log('not called'); },
});
const obj = {
x: 3,
} |
Or Object.create |
I repeat my earlier claim of the uselessness of analogizing to code that nobody writes. Neither behavior is unambiguously "correct"; there are multiple plausibly-analogous behaviors we can point to (constructor assignment, prototype chaining, object literal property assignments, simple assignment, etc). The question is which behavior is going to be dangerous or unexpected. Are people writing Are people writing Are people writing Should the behavior of an initializer be like the common thing that people do all the time, or should it be like a dangerous other uncommon thing that is going to subtly break base class behavioral expectations? |
When the issue of define vs set first came up we explored it quite a bit. The conclusion we ultimately came to was that neither is 100% obviously better than the other (both have decent arguments for and against them), but also those pros/cons aren't exactly fatal. SetThe predominant argument in favor of [[Set]] is that it satisfies use cases similar to the following:
It's reasonable for a user to expect that this would create a DOM text node with the text
This is roughly equivalent to what users must do today in this situation, and one could even argue that since the subclass doesn't "own" the field it probably shouldn't be declaring it either. DefineThe main benefits of using Define were that it leaves room for the addition of const fields (and Mark's const classes) in the future, and it plays a little more seamlessly with decorator-descriptors. If we were to go with Set and one day were to add const fields, it would lead to some pretty surprising/problematic refactoring cases if one updated So at the end of the day, you really can make reasonable and non-fatal arguments in favor of and against both of these things. So the fact that going with Set could easily make it more difficult to add const fields in the future and somewhat simplifies the interactions between field initialization and decorators, we chose to go with define. |
Where are these proposals? Links? |
If you want |
The reason I should also note that |
@jeffmo While if you dismissed |
Yes I agree, I think a future const fields proposal should probably dismiss the Let’s try to keep this thread on topic though :) |
There's some debate over what the semantics should be. I wanted to try to collect arguments in one place.
=
implies Set.:
).foo
to maintain some invariant, some people might be surprised that adding an initializer forfoo
shadows it instead of triggering it. Also, library authors use setters to deprecate properties (by allowing their use but printing a warning), and subclassers will only get those warnings if initializers use Set.{ set foo(x){}, foo: 0 }
does not trigger the setter.We also have the option of some hybrid strategy: for example, throwing if an initializer would overwrite another property and/or throwing if an initializer would shadow a property from the prototype.
The text was updated successfully, but these errors were encountered: