Open hax opened 4 years ago
I disagree, and would be confused by the restriction.
I accept that constructors have differences from methods here - there's no .call
/.apply
variant for new
, and subclasses aren't allowed to access this
before super()
. But neither of those change how the this
parameter acts once it's available; importantly, it still suffers from the exact same shadowing problems that motivate this entire proposal.
Whether you're declaring a class within another method, or declaring a this
-using function within a constructor, you'd still benefit from being able to rename the constructor's this
; in particular, the case of "define a class inside the constructor of another class" would, with the current restriction, have an unavoidable clash, requiring you to do the silly old let self = this;
dance.
Overall this seems like an unnecessary semantic distinction that makes the syntax feel less unified, for no benefit.
@tabatkins I understand your concern, but there will be more weirdness if we support explicit this in class constructor, for example, if we support constructor(this x) {}
, it seems we should also support constructor(this {foo, bar}) {}
, or even constructor(this {foo = 1, bar = 2})
, but what the reasonable behavior should be?
I'm much more fine with disallowing that, since it's meaningless. (The value doesn't meaningfully have properties yet, or is in a TDZ.) Differences when things actually act different are great. ^_^
@tabatkins Could you make clear about that ? Do you mean you prefer allowing rename of this
argument of class constructor but disallowing destructure of this
argument? Doesn't that also make syntax feel less unified?
since it's meaningless.
But neither of those change how the this parameter acts once it's available
@tabatkins I feel these two sentences contradict each other.
Strictly speaking, there is no this
argument/parameter for class constructor at all ^_^ . this
values in constructors are generated by the new
mechanism of classes/base classes. So even we really want some syntax for renaming this
in constructors, we'd better not use the syntax of argument/parameter. But to be honest, I don't find any other syntax alternatives better than current, especially current syntax of constructor(this) {}
have already been implemented by TS and many other tools for many years.
Sure there is - the new
passes a new one, and if you invoke a non-class constructor as a function with .call or similar, it gets a different one.
Yeah, constructors have an implicit this
argument, exactly like any other method, and so have the same arguments for renaming it as any other method does. The source of this
doesn't matter; whether it's "generated by new
" or passed in by the foo.bar()
syntax or something more explicit, it's still a this
argument and acts almost exactly the same. I think it will confuse people if renaming this
did not work in this case.
The only difference from other methods is that there is a TDZ for this
if the class is a subclass; in such cases there's no way to destructure the this
. Since there's not really an argument for destructuring the this
of a class constructor even when there is no TDZ, I think it's reasonable to restrict that functionality entirely in this case; I do not think people will be confused by destructuring this
not working in this case.
(General "function called with new
" cases should still allow the full set of renaming+destructuring; it's just class constructors specifically that we should separate out here.)
It seems we have different understanding of what "this argument/parameter" mean.
My understanding is based on the current syntax and APIs of JS:
foo.bar()
foo.bar.apply(foo)
foo.bar.call(foo)
foo.bar.bind(foo)
Reflect.apply(foo.bar, foo)
In all those cases, foo
is the "this argument" passed to foo.bar
.
On the other hand
new foo.bar()
Reflect.construct(foo.bar)
In those cases, programmers can't pass "this argument" to foo.bar
.
I am not sure how to express the impact to programmers mind of these two usage in more accurate words, but I truly believe it's very important to differentiate them in programmer's perspective.
the
new
passes a new one, and if you invoke a non-class constructor as a function with .call or similar, it gets a different one.
@ljharb I don't understand how new
could pass "this argument" to a function. And this issue is focused on class constructor, invoking .call
on class constructor always throw. I changed the title to make it clear.
The user using new
isn't passing one, but the function itself is receiving one - this
is the receiver, from the perspective of the function. How it's passed isn't conceptually material imo.
@ljharb You are describing the mechanism, technically we can still name such this
in new
as "this argument" in spec, but I feel it's really confusing to name something not passed by the caller as "argument/parameter" in the programmer's perspective.
Sure, but that's something this proposal is doing - it's not necessarily an already established definition. this
as "receiver", however, is, as is looking at this
as something that's defined for the function's perspective regardless of how it's generated.
So it's more like a naming issue. Maybe we could make it clearer by renaming this proposal from "explicit this" to "explicit this argument" if it could help to solve the confusion we meet here.
The proposal text written by @gilbert 4 years ago never discuss about class constructor before my PR, also leave many details unspecified like whether supporting default parameters (created a new issue #8 ). And there were some other syntax alternatives which may do not have the implication from "argument"-like current syntax. Anyway, we are only in stage 0 now, as I understand this issue is more like a stage 2 block issue :)
Also, I see similar discussion of "this argument" in https://github.com/hax/proposal-function-this/issues/1#issuecomment-579123649 .
I do not think people will be confused by destructuring this not working in this case.
@tabatkins As my experiences, it's dangerous to affirm people will not be confused in some specific way :) The community is too large with big variance. For example, React guys complains about forcing super()
in class components. If it's a burden for some parts of the community to remember this specific differences between base classes and derived classes, then we could infer that they will probably be also confused by allowing destructuring in base classes but not derived classes.
We could also forbid destructuring totally in all class constructors, but some parts of the community may also be confused. Of coz we could have some faqs to explain that destructuring have TDZ issue in derived classes and also have no use case in base classes (I suppose), but it will introduce many extra criteria of the feature. For example, it also seem no much use cases of constructor(this) {}
? Shall we also forbid it, and only support constructor(this renaming) {}
?
Even we only support constructor(this renaming) {}
, we also have an issue of future runtime features like parameter decorator, what's the runtime semantic of constructor(@deco this renaming)
in derived classes? We may face similar TDZ issue like destructuring. Of coz we could forbid that case again or define some magic happen after super()
(if introduce magic then such magic should also make destructuring work), but we would just increasingly introduce special rules. I feel it will also increase the complexity of the spec.
if renaming this did not work in this case.
Actually not only renaming, my proposing is disallow all explicit this syntax for class constructors.
this
in constructors is very different than others:
this
value is an incomplete object in constructors which require specific attention when coding. For example, as typical OO theory you should avoid to call any virtual methods on this
in constructors.this
instance and implicitly return this
as result (though they could choose to dismiss this
by returning another object), while normal methods/functions only use this
as normal reference and "send messages" (use OO term) to it.With all these differences, I feel it's better to leave this
in constructor untouched at all.
I have to admit there will be always some weirdness in all options, but I feel disallowing this
in class constructors may be the simplest one, and we should notice that it is the only option which allow us change to other options in compatible way if we really want.
It is not passed by the caller, but generated by itself or its base classes.
This seems irrelevant to the argument; this proposal has nothing to do with how you initialize the this
value for a function. Can you elaborate on why you think this distinction means people shouldn't rename the this
value?
TDZ in derived classes.
Yes, that's a reason to disallow destructuring of this
in class constructors. And I recognize that, due to the inconsistency it introduces, it does contribute to the argument to avoid touching this
at all.
this value is an incomplete object in constructors which require specific attention when coding. For example, as typical OO theory you should avoid to call any virtual methods on this in constructors.
Irrelevant to the argument, as far as I can tell. You use this
in the constructor all the time; whether or not it's "complete" has nothing to do with what you want to name it. Again, can you elaborate on why you think this distinction means people shouldn't rename the this
value?
(If anything, this might be an argument that people are unlikely to try to destructure this
in constructors, and thus we shouldn't worry about them being confused that it doesn't work...)
Constructors initialize the this instance and implicitly return this as result (though they could choose to dismiss this by returning another object), while normal methods/functions only use this as normal reference and "send messages" (use OO term) to it.
Also, as far as I can tell, irrelevant to the argument. Sorry to ask again, but can you elaborate on why you think this distinction means people shouldn't rename the this
value?
Basically, my problem is that when someone is confused and asks "why can't I rename this
to self
in a constructor like I do with all my other methods?", answering with "because you can't destructure this
in subclass constructors" is a non sequitur.
There are good reasons for cutting off technically-usable functionality in service of a simpler rule about the functionality (I've done it plenty of times in CSS, sometimes against significant opposition), but I don't believe this case justifies it. The line between usable and unusable functionality isn't complex, and I don't think it'll become more complex in the future, and the potential for confusion due to the missing functionality feels too high.
(For example, in CSS's calc()
function we require whitespace around +
and -
operators, to reduce the chance of confusing parses. Technically, it's sometimes okay; calc(5- 3)
would parse as a number, a "-", and a number just fine. But calc(5-3)
parses as a number and a negative number (CSS doesn't have unary minus as an operator), and calc(5em- 3em)
parses as one value with the unit "em-" and another with the unit "em". Allowing 5- 3
wasn't judged valuable to authors, so rather than allow it but also allow the possibility of those mistakes, we just required spaces in all cases. "+" doesn't even have as much confusion as "-", but it shares some of the confusing cases, and since + and - are intrinsically linked in people's minds, a blanket rule covering both of them was considered best. But we didn't require that for "" and "/", because they have no* parsing problems at all.)
@tabatkins None of my comments say people shouldn't rename the this
value in constructor. I'm sorry if I don't make it clear.
What I need to clarify is renaming this
is only one part of the feature, it may be emphasis too much in original proposal text. I believe the most important sentence should be "its primary use case is to play nicely with the function bind proposal", as this sentence, class constructors are not the "primary use case".
Of coz we could adjust the goal of a proposal in current stages. I just want to be clear that many use cases of this proposal (like parameter annotation and decorator) do not relate to renaming.
I listed some differences of this
in between constructors and methods to explain why we'd better rule out constructors totally, I'm not discussing renaming, but the whole feature. You could notice that in the original issue text, I never use renaming syntax.
Current proposal proposed three syntax, f(this)
, f(this name)
, f(this {foo, bar})
(could also be any destructuring like f(this [foo, ...bar])
), but we actually could explain the proposal as one sentence: Allow you treat implicit this
parameter same as normal parameters, so you can:
(The only exception is providing default value, see #8 )
I think it's not so hard to explain to programmers why we don't support it in constructors: because constructors do not have "this parameter" (let me use "this parameter" which seems a little bit unambiguous than "this argument").
On the other side, it's not easy to explain/teach/infer the behavior for this
in constructors if we support only some syntax combinations, you may need a table!
syntax | base classes | derived classes | |
---|---|---|---|
annotate | constructor(this: T) |
no? | no? |
decorate | constructor(@deco this) |
yes? | no? or yes (with some magic after super() ) |
name | constructor(this x) |
yes | yes (with TDZ) |
destructure | constructor(this {foo, bar}) |
no? | no? or yes (with some magic after super() ) |
Note, though I don't want support default value initializer, it's possible to support it. But if we want both default value and constructors, we will need another rule, because there is no reason to support default value in constructors (this
in constructors won't be undefined
anyway).
my problem is that when someone is confused and asks "why can't I rename this to self in a constructor like I do with all my other methods?",
With some clarification of the proposal name (renaming the proposal from "Syntax for Explicitly Naming this
" to "Explicit this
parameter") and related text, the answer could be:
A: Not only renaming, you can't use any explicit this
parameter syntax in class constructors at all.
Q: So why we can't use them?
A:
Short version: Because constructors are not methods. ^_^
Long version: Because constructors do not have "this parameter", only methods have "this parameter".
Extra explanation:
Theoretically we could still support renaming this
in constructor with same syntax, but it would be hard for the author of this proposal to define (and hard for programmers to learn/infer/remember) the behavior of different combination of features like parameter annotation/decorator/destructuring in different cases (base classes VS. derived classes). See the table in previous comment.
Note, while you can't use any explicit this
parameter syntax in class constructors, at the same time, functions with explicit this parameter do not have [[Construct]]
, so invoking new f()
will throw if f
is a function with explicit this parameter. Explicit this parameter and new are mutually exclusive, because class constructors can only be invoked by new, so Explicit this parameter and class constructors are also mutually exclusive.
answering with "because you can't destructure this in subclass constructors" is a non sequitur.
Sorry, I didn't mean to use only destructuring issue as reason, just used it as a example of the consequences of supporting explicit this in constructors.
There are good reasons for cutting off technically-usable functionality in service of a simpler rule about the functionality
Agree.
but I don't believe this case justifies it.
Hope this comment could justifies it ^_^
"methods" aren't really a thing; that's just a colloquialism for function-valued properties that tend to use their receiver, but can be "borrowed" and used with potentially any receiver.
"methods" aren't really a thing; that's just a colloquialism
@ljharb
Yeah! I use "methods" because the original question use this word.
But there is another interesting thing, it seems "method" in spec means functions with specific syntax like class methods and let o = { method() {}, notMethod: function () { this } }
. method()
is a method even it doesn't expect this argument to be passed in, notMethod()
is not a method even it does expect this argument. ^_^
So there is a gap between "method" in spec and the "method" in programmer's mind.
This problem bother me, I used to consider API name "Function.isMethodLike(f)" for the functionality of thisArgumentExpected, but obviously it was a bad idea. (Though I am not sure whether current name "thisArgumentExpected" is clear enough, we could discuss it in that repo)
"Concise methods" still require a receiver, and can still be .call
ed, so that distinction isn't particularly useful at this time.
@ljharb I modified previous comment to make my meaning clearer :)
Off-topic: About calc()
in CSS.
@tabatkins
I happened to answer the question about the issue of +/-
in calc()
in Chinese CSS community 7 years ago (https://www.zhihu.com/question/21636985/answer/18841264). I remember that because some very senior programmers who can read spec well were also confused (it seems CSS grammar used to have a bug, miss the leading "+/-" in "num", which cause the confusion).
The problem of calc()
in practice is calc(3em-1px)
not work as programmer expect (while calc(3em*2)
works). Theoretically calc()
was a new syntax so it could introduce some contextual parsing rules for +/-
to make calc(3em-1px)
work as expect, though it may introduce other issues I don't know. The main consideration I guess is to keep the simpilicity of CSS grammar/parser.
If we can't make calc(3em+1px)
work as expect then we'd better make the correct syntax simple, so I very appreciate your design of forcing spaces in both side and make calc(5- 1)
as error! If allowing calc(5- 1)
, there will be room for tools using different policy, which do not have much benefit but only cause problems for the authors.
I would like also enforce spaces around *
and /
for simplicity and consistency in author's perspective, though I understand the reason of current design. And authors could use tools to enforce that if they want.
I think JS also have similar cases. Especially what constraints we should put in language and what constraints we could leave to tools.
In this specific issue, I chose to rule out class constructors totally, instead of including constructors but only allowing some special syntax combination. Actually I feel it's similar to your design decision of forcing spaces around both side and not allowing specific cases like calc(5- 1)
^_^
Of coz calc()
case is much clear because you could always write calc(5 - 1)
instead of calc(5- 1)
, in this case, we can not use "explicit this parameter" in constructors so programmers still need to write imperative code like const obj = this
or const obj = super()
to create an alias for this
in constructors if they really want.
I think it's not so hard to explain to programmers why we don't support it in constructors: because constructors do not have "this parameter"
You've said this more than once, and I don't understand what you could possibly mean by it. Constructors absolutely have a this
parameter. Within the body of a constructor this
is magically bound to a value, exactly like any other method.
Theoretically we could still support renaming this in constructor with same syntax, but it would be hard for the author of this proposal to define
Spec authors are the absolute lowest tier on the priority of constituencies; unless there's a very good reason, making a feature better for implementors, script authors, or webpage users absolutely wins over making a spec author happier. If you think you'll have trouble writing the details, you can ask for help.
(and hard for programmers to learn/infer/remember) the behavior of different combination of features like parameter annotation/decorator/destructuring in different cases (base classes VS. derived classes).
I'm not proposing any different combination of features for those cases. My proposal is that all class constructors allow renaming, and none of them allow destructuring or any other argument manipulation, like decorating. That's it.
I believe it's easy to understand the line being drawn - constructors can't manipulate their this
argument, just rename it.
I don't understand what you could possibly mean by it. Constructors absolutely have a
this
parameter.
I'm really confused. Why something never can be passed in could be treat as function parameter/argument? Maybe I understand the terminology of "function parameter/argument" in wrong way?
Within the body of a constructor
this
is magically bound to a value,
I understand it is "this value" as spec terminology, not "this parameter" or "this argument".
exactly like any other method.
Any other methods do not have such magic.
it's easy to understand the line being drawn - constructors can't manipulate their
this
argument
I don't think it's easy, or at least not much easier than understanding the magic this
of constructors vs any other methods.
For example, programmers will ask why we can't destructuring this
in constructors of derived class? They may want to use properties of base cases.
I'm really confused. Why something never can be passed in could be treat as function parameter/argument? Maybe I understand the terminology of "function parameter/argument" in wrong way?
In:
class Foo {
constructor() {
console.log(this);
}
bar() {
console.log(this);
}
}
var x = new Foo();
x.bar();
The log will show the Foo object twice, because in both cases, this
is "automatically passed in" (really: automatically bound in the context of the function body).
@tabatkins
As I understand, the expression x.bar()
pass the value of x
as "this
argument" with other arguments (in this example, no other arguments are provided) to bar
method, which is basically same as (Foo.prototype.bar).call(x)
or Reflect.apply(Foo.prototype.bar, x)
. I call it "this
argument" because the spec and the docs use such name:
spec:
19.2.3.1 Function.prototype.apply ( thisArg, argArray )
26.1.1 Reflect.apply ( target, thisArgument, argumentsList )
MDN:
func.apply(thisArg, [ argsArray])
Reflect.apply(target, thisArgument, argumentsList)
As those API names, there are "this
argument" and "the list of (normal) arguments" for a function call, you need to specify the values for both "this
argument" and other normal arguments.
On the other side, when you invoke new Foo()
(or Reflect.construct(Foo)
), there is no way to specify a value as "this
argument" to pass in, there is also no thisArg
or thisArgument
in the API signature of Reflect.construct(target, argumentsList[, newTarget])
.
this
is "automatically passed in"
As a non native English speaker, I'm not sure whether new Foo()
case could be interpreted as "passed in". Maybe you mean when you invoke new Foo()
, there is something generated then "passed in". But it's weird to me, with similar logic we could also say there is this
"passed in" to () => this
.
This is why I don't understand how this
in class constructors could be called as "this
argument/parameter" of that constructor. The terms I could accept are "this
value", "this
binding", "this
reference".
Hope you can get me.
Anyway, if "this
argument/parameter" is not a good term, is "receiver" a better term? Actually Java call it "receiver parameter". (And in Java, constructors also can't have "receiver parameter".)
Spec authors are the absolute lowest tier on the priority of constituencies
@tabatkins
I really love the priority of constituencies and hope all our process of web standards include JS could follow the principles well... (omit some complains to some proposals :-)
What I really worry about is, in this case, if it's hard for us to define all possible syntax/semantic combination in a consistent way, it will also be very hard for programmers to learn/infer/remember the behavior of the feature, especially there are already too many FUD about this
-related things.
I think we two have the agreement that class constructors are special. The divergence is how to deal with it.
You propose only allow renaming this
in constructors but ban other syntax/features.
I propose ruling out constructors entirely.
I have given several reasons why I prefer that, and try to use a corresponding term ("explicit this
parameter" or "receiver parameter" or something else we could agree) instead of overbroad term "explicit this
" for that. (But even without this issue, I think we should also use a term follow the prior art of similar features of TypeScript/C#/Java.)
One of the important reason I like to repeat is, ruling out constructors is forward-compatible. We could introduce renaming this
for class constructors or other feature relate to this
for class constructors in future proposal after we have real world feedback of all related features. For example, we may still need to wait several years for parameter decorator. Currently I can't imagine any solid use case of parameter decorator for this
in constructors, but I feel we are not the correct people to make such decision in current status. So I feel postpone the problems of this
in constructors to the future proposal may be a better solution.
I think the confusion is between arguments and parameters. A constructor has no this parameter; it has a this argument. (i think you could swap the definitions and it’d still make sense; i don’t know if there’s a universally understood definition for which term is which)
A constructor has no this parameter; it has a this argument.
Does the spec draw some distinction between arguments and parameters? As someone who's not deep in the technical weeds of the spec, I have no idea what that difference would be. I just know that both constructors and methods get something called this
automatically bound in their bodies.
Maybe you mean when you invoke new Foo(), there is something generated then "passed in". But it's weird to me, with similar logic we could also say there is this "passed in" to () => this.
No, there's a big difference between a function closing over an existing variable that appears in its outer context (what you get with () => this
), and a function generating a brand-new binding for a variable named this
in its body (what both methods and constructors do).
On the other side, when you invoke new Foo() (or Reflect.construct(Foo)), there is no way to specify a value as "this argument" to pass in, there is also no thisArg or thisArgument in the API signature of Reflect.construct(target, argumentsList[, newTarget]).
Yeah, I think it's totally reasonable that there's no way to pass in a special value for this
in a constructor; the whole point of a constructor is to generate a fresh version of the object.
But I repeated this question multiple times in my previous comment for a reason: can you elaborate on why you think this distinction means people shouldn't rename the this
value?
All you're doing so far is pointing out that constructors and methods are different. I agree, they're different. But inside the function, they both automatically create a binding for a variable named this
. Your proposal lets people rename that binding (among other things) in methods. Why do these differences mean people shouldn't be able to rename the binding in constructors?
@tabatkins tbh i confuse myself often with this. the main point is, one thing is passed in, and the other is received. Whether a this
is passed in or not, all non-arrow functions receive one.
TypeScript do not allow
this
parameter in constructor.It's very clear in derived class case:
In base class case, even we can access
this
in the very beginningIMO we should also not allow explicit
this
in base class, becausethis
in the constructor is never a parameter or argument passed by caller, but generated by the constructor itself.Actually I feel we should make explicit
this
and constructor mutually exclusive, which means forwe should not allow
new f()
(throw TypeError) because in thenew f()
form,this
is not an argument/parameter.Essentially we should make
f
have no[[Construct]]
internal method and noprototype
property just like arrow functions, methods and accessors and all built-in non-constructor functions.So explicit
this
will also provide a way to allow programmers clearly mark a function as a "method" without forcing them use method syntax.