Should the default constructor return the return value of super?
You mean return-if-undefined, right? Not sure if that adds a dimension of oddity to the idea that might change your mind.
(Obviously I'm not a fan of returning from constructors in the first place -- but that's just because I think it's generally a code smell. People do it even if I don't)
Ok I stand corrected -- I didn't think to consider that returning undefined is the same as not returning.
I agree that constructor(...args){ return super(...args); }
is better (i.e. less surprising) when the super-class's constructor returns an other object than this
. However, under the current state of the specification, there is at least one exception: the Object
constructor ignores its this
value and creates a fresh object, which is unwanted in the theoretical case you define a class extending explicitly Object
. I guess that this case could be normalised by using a similar mechanism as Array
and its [[ArrayInitialisationState]] internal slot?
Additionally, since DefineMethod is going to be exposed, does it make sense to expose ReferencesSuper too?
Otherwise there is no way to detect, at runtime, if a default constructor function is a top-level constructor (without a super), or a subclass with a super. One of them can use toMethod but not the other.
Since I haven't yet seen any formal definition of toMethod
, I don't what is its intended behaviour on methods that don't reference super
. I think that the most reasonable behaviour, in that case, is to just clone the function: for it would allow to refactor transparently methods, removing or re-adding references to super
without needing to adapt the code around them.
Claude Pache wrote:
I agree that
constructor(...args){ return super(...args); }
is better (i.e. less surprising) when the super-class's constructor returns an other object thanthis
. However, under the current state of the specification, there is at least one exception: theObject
constructor ignores itsthis
value and creates a fresh object, which is unwanted in the theoretical case you define a class extending explicitlyObject
. I guess that this case could be normalised by using a similar mechanism asArray
and its [[ArrayInitialisationState]] internal slot?
I thought Allen designed things so
class C {}
differed from
class C extends Object {}
so as in the first case to avoid (a) super calling Object and making a useless newborn; (b) C inheriting class-side properties from Object.
On Dec 24, 2013, at 6:41 PM, Sebastian Markbåge wrote:
This is an esoteric and ugly use case but I'm not trolling. The default constructor for a class which extends another is:
constructor(...args){ super(...args); }
Is there any reason it shouldn't return the value from super?
constructor(...args){ return super(...args); }
Basic constructors still have the quirky behavior of ES functions that they can return any object and don't have to return the instantiated object. This can be useful if they're used as functions or should return a placeholder object, or other instance, for compatibility/legacy reasons. E.g. when you have a custom instantiation process.
class Foo { constructor() { return {}; } }
This sort of allocation in the constructor body of a class definition will probably come to be viewed as an anti-pattern. Note that:
new Foo instanceof Foo
would evaluate to false given the above class definition. Also, the Foo.prototype
would not be on the prototype chain of the returned value.
There is a new factoring of responsibility and associated usage patterns that come along with ES6 class definitions. We should no longer think in terms of a constructor function having the responsibility for both providing a new instance and initializing its state. It is the responsibility of the @@create method to provide the object that is returned by the new operator. The primary responsibility of the constructor body is to initialize that object.
A better definition of the above Foo would be:
class Foo {static [Symbol.create]() {return {}}}
However definition still produces instances that fail the instanceof test, so perhaps what you really want would be:
class Foo {static [Symbol.create]() {return {__proto__: this.prototype}}}
It is difficult to design of a constructor body that behaves correctly in all five of these situations: invoked via the new operator; invoked with a super call from a subclass constructor; called directly; called via call/apply function with arbitrary things passed as the this value; and, called as a method. The ES6 spec. has to handle all for of those use cases for the legacy built-in constructors. But I don't think we want to encourage people to do so for new abstractions defined using ES6 class definitions because in most cases what they produce will be buggy,
The usage patterns we should be teaching for class abstractions in ES6 are:
- Always use the 'new' operator to create instances of a class.
- Never call a class directly by name without 'new.
- Class constructor bodies should initialize the instance object that are passed to them.
- Class constructor bodies should only be called by the 'new' operator or as a 'super' call from a subclass constructor body.
- [experts] Define a @@create method if you need your class to have special allocation behavior.
Also, don't model your classes after Array, Date, RegExp, Error, etc. as they violate these rules. Unlike those built-ins, don't design your classes such that 'new' is optional. It is hard to get that right while still permitting subclassing.
Regarding adding 'return' to the default constructor body. It appears that technically it would be a benign change. However, the only reason to do so would be accommodate superclasses that deviate from the above patterns. In that case, you are probably already in the weeds. I'm not sure that we should be trying to facilitate such deviations.
Currently, this behavior doesn't carry over to subclasses by default:
class Bar extends Foo { }
You'd have to explicitly define a constructor that returns the value from the super call.
Additionally, since DefineMethod is going to be exposed, does it make sense to expose ReferencesSuper too?
Otherwise there is no way to detect, at runtime, if a default constructor function is a top-level constructor (without a super), or a subclass with a super. One of them can use toMethod but not the other.
Can you describe the use case you are thinking about here? The decision to do a super call in a constructor is normally a design time decision made by a programmer.
I completely agree that is the intended use and what we should be encouraging people to do. What I'm asking for is to intentionally break best-practices for a specialized use case.
The use case I had in mind was React components. Components in React are described as classes which makes them seem approachable to a broad user base. They cannot and should not be accessed as class instances though. The instances are immutable data structures used exclusively by the library. The base constructor could look something like this:
constructor(x) {
return { _hiddenInstance: this, _instantiationContext: CurrentContext, _id: uid(), _someArgument: x };
}
This would generate a descriptor that can be used by the library but only used as a reference by the user. This allows users to declare classes just like they're used to and even instantiate them normally. However, they'd only be given access to the real instance at the discretion of the library.
Of course, we could have all users wrap their classes in some kind of decorator constructor and that's probably where we'll end up for clarity. It's would've been a neat pattern though.
Regarding adding 'return' to the default constructor body. It appears that technically it would be a benign change. However, the only reason to do so would be accommodate superclasses that deviate from the above patterns. In that case, you are probably already in the weeds. I'm not sure that we should be trying to facilitate such deviations.
It seems to me that whether we add it or not is arbitrary. By not adding it we're intentionally removing this use case (forever). IMO we need to have a reason to intentionally prevent a use case if it could be easily supported. I'd buy almost any argument here except that it's in bad taste to use this pattern.
If we have reason to believe that this pattern will be harmful, do we also have to do more to prevent normal constructors from returning anything other than "this"?
On Fri, Dec 27, 2013 at 10:22 AM, Allen Wirfs-Brock <allen at wirfs-brock.com>wrote:
Exactly, see step 7 of people.mozilla.org/~jorendorff/es6-draft.html#sec-runtime-semantics-classdefinitionevaluation
I think the differences should include to making calls to super() illegal when 'extends' is absent:
class C { constructor(url) {super(url);}} // legal, I expected illegal.
On Dec 28, 2013, at 5:35 AM, Sebastian Markbåge wrote:
I completely agree that is the intended use and what we should be encouraging people to do. What I'm asking for is to intentionally break best-practices for a specialized use case.
The use case I had in mind was React components. Components in React are described as classes which makes them seem approachable to a broad user base. They cannot and should not be accessed as class instances though. The instances are immutable data structures used exclusively by the library. The base constructor could look something like this:
constructor(x) { return { _hiddenInstance: this, _instantiationContext: CurrentContext, _id: uid(), _someArgument: x }; }
This would generate a descriptor that can be used by the library but only used as a reference by the user. This allows users to declare classes just like they're used to and even instantiate them normally. However, they'd only be given access to the real instance at the discretion of the library.
With the current ES6 spec. you can accomplish the same thing via:
static [Symbol.create]() {
return {
_hiddenInstance: super(), //this will create the "normal" instance
_instantiationContext: CurrentContext,
_id: uid()
} //this is the object that is passed as the 'this' value to the constructor
};
constructor(x) {
this._someArgument = x
}
Of course, we could have all users wrap their classes in some kind of decorator constructor and that's probably where we'll end up for clarity. It's would've been a neat pattern though.
Regarding adding 'return' to the default constructor body. It appears that technically it would be a benign change. However, the only reason to do so would be accommodate superclasses that deviate from the above patterns. In that case, you are probably already in the weeds. I'm not sure that we should be trying to facilitate such deviations.
It seems to me that whether we add it or not is arbitrary. By not adding it we're intentionally removing this use case (forever). IMO we need to have a reason to intentionally prevent a use case if it could be easily supported. I'd buy almost any argument here except that it's in bad taste to use this pattern.
If we have reason to believe that this pattern will be harmful, do we also have to do more to prevent normal constructors from returning anything other than "this"?
I'm not exactly sure where who you see ES6 classes fitting into React. Whether you are talking about using them at the meta level to implement React or whether you want to replace React.createClass with something like:
class MyComponent extends React.Component {
render() { ... }
}
The code I gave above should work in the latter case (although I suspect you are really doing something more complex with the constructor argument). However, to create an instance you would have to say
new MyComponent({})
instead of
MyComponent({})
So this makes me think that you may have a different integration in mind. Perhaps:
let MyComponent = React.create(class {...});
I think the topic of who ES6 class definitions (and other features) integrate with frameworks that provide their own abstraction mechanism is an important one so it would be good to explore this further in the context of React.
When I started trying to articulate why I don't like this idea, I wound up convincing myself that my issue is just that I'm not fond of the ability to return from constructors in the first place for all the reasons we seem to agree on. So I don't like this idea because it furthers something I don't like about existing ES; Thus it became pretty difficult for me to objectively justify the idea that ES6 classes shouldn't return super() by default given that one still can do this in non-default constructors.
OTOH, if ES6 class constructors didn't allow returns, it seems it would justify not doing this much more. When refactoring to classes, this type of behavior should be statically detectable and seems pretty easily worked around via wrapper functions for the majority of cases.
So, if we really want to discourage the return-from-constructor behavior (which I would be very much in favor of), it seems like we should also disallow returns from ES6 class explicit constructors as well.
On Dec 28, 2013, at 7:38 AM, John Barton wrote:
I think the differences should include to making calls to super() illegal when 'extends' is absent:
class C { constructor(url) {super(url);}} // legal, I expected illegal.
Plausible, but what about the possibility that a framework dynamically patches the prototype chain of C.prototype before it is first instantiated or if C uses its @@create method to return instances that have different or extra objects (that have a 'constructor' property) patched into their prototype chain.
This, like the base question of this thread, seems like another example of a tension between supporting the "normal" usage patterns for class declarations and allowing enough flexibility to enable using class declarations as building blocks for less normal abstractions.
I could probably go either way. Pragmatically, from a spec. writing perspective, the conditions for issuing this error are bit complicated to define. Even without an extends clause the instances still inherit from Object.prototype so super call still need to be allowed in non-constructor instances methods and even in constructor methods things like super.toString() need to be allowed. I generally try to avoid early errors that have a lot of conditions associated with them.
Allen Wirfs-Brock wrote:
I generally try to avoid early errors that have a lot of conditions associated with them.
That's a warning sign in JS specs, indeed.
Adding class syntax as (mostly) sugar for the prototypal pattern does
not obviously mean rejecting all unusual or exceptional variants
possible in the prototypal pattern. I say let super
be used even in
class C{}
's constructor, and let return-from-constructor work without
requiring [Symbol.create]
. JS is dynamic.
On Sat, Dec 28, 2013 at 8:35 AM, Sebastian Markbåge <sebastian at calyptus.eu> wrote:
I completely agree that is the intended use and what we should be encouraging people to do. What I'm asking for is to intentionally break best-practices for a specialized use case.
The use case I had in mind was React components. Components in React are described as classes which makes them seem approachable to a broad user base. They cannot and should not be accessed as class instances though. The instances are immutable data structures used exclusively by the library. The base constructor could look something like this:
constructor(x) { return { _hiddenInstance: this, _instantiationContext: CurrentContext, _id: uid(), _someArgument: x }; }
This would generate a descriptor that can be used by the library but only used as a reference by the user. This allows users to declare classes just like they're used to and even instantiate them normally. However, they'd only be given access to the real instance at the discretion of the library.
We're doing this with Maps (for non-DOM bound data) and WeakMaps (for DOM bound data) in the FirefoxOS Messages app (for Recipients, Threads and Draft data objects).
I'm not exactly sure where who you see ES6 classes fitting into React.
We've explored using:
class MyComponent extends React.Component { };
let MyComponent = React.createClass(class { });
let MyComponent = React.createClass({ method() { super(); } });
Your @@create
example convinced me that we're better off using the first
one together with @@create
. That will allow us to delay allocation of the
hidden instance.
Adding class syntax as (mostly) sugar for the prototypal pattern does not obviously mean rejecting all unusual or exceptional variants possible in the prototypal pattern. I say let
super
be used even inclass C{}
's constructor, and let return-from-constructor work without requiring[Symbol.create]
. JS is dynamic.
An example where super
can be used in class C { }
would be if you use an
existing class descriptor to later create a specialized framework class out
of it. An existing framework may have an internal base class that their old
syntax automatically added. This could allow them a plumbing function to
turn class descriptions into framework specific classes.
class C { constructor() { super(); } };
Framework.registerComponentTypes({ 'C': C });
Either by using setPrototypeOf
, or using toMethod
on the constructor.
I made the change in rev 22 to make the default constructor: return super(...args)
However that resulted in this bug: ecmascript#2491 from Arv:
The problem arises when we extend an old school "class" where the code does not explicitly set constructor.
function B() {} B.prototype = { ... } class C extends B {} new C() instanceof C // false
The reason why this fails is that
B.prototype.constructor === Object
sonew C()
returnsObject()
.The work around is to set
B.prototype.constructor = B
but I feel like the problem, adding return added solved, is smaller than the problem it introduces.
I'm inclined to agree with Arv's conclusion. What do you think?
I think this change should be removed. In fact, I didn't realize this was
actually added to the spec draft--I haven't had the time to review this
draft as I usually do. The fact that JavaScript currently requires
B.prototype.constructor = B
to get the right constructor is generally
seen as a language bug (whether that's accurate or not, I don't care to
argue the point, subjectively it sucks). While I completely understand the
motivation for the requested change, it's an edge case at best and
apparently I missed the part of the conversation where there was an
agreement to make the change.
+1 for removing implicit return of the super ... if that's really needed, and AFAIK returning object variables from constructors is an exception, not the common case, it can always be explicitly done inline without undesired shenanigans
just my $0.02
Le 2 févr. 2014 à 23:27, Allen Wirfs-Brock <allen at wirfs-brock.com> a écrit :
I'm inclined to agree with Arv's conclusion. What do you think?
I think that this bug is indeed a good reason to use super(...arguments)
instead of return super(...arguments)
.
However, I am slightly worried that, with the ES6 class syntax, the constructor
method looks like a regular method, but it behaves differently w.r.t. inheritance. So, nonstandard uses of this method (wmay mysteriously fail when used on a super-class.
Therefore, it would be nice if return expression
would be statically forbidden in the constructor
method when defined using the class
syntax (but a bare return
is fine, which slightly complicates the grammar).
On Feb 3, 2014, at 10:22 AM, Claude Pache wrote:
I think that this bug is indeed a good reason to use
super(...arguments)
instead ofreturn super(...arguments)
.However, I am slightly worried that, with the ES6 class syntax, the
constructor
method looks like a regular method, but it behaves differently w.r.t. inheritance. So, nonstandard uses of this method (wmay mysteriously fail when used on a super-class.
not really, how do you mean?
Therefore, it would be nice if
return expression
would be statically forbidden in theconstructor
method when defined using theclass
syntax (but a barereturn
is fine, which slightly complicates the grammar).
I don't think that restriction is really needed. Even within a class declaration, an explicit value return in a constructor body has a consistent meaning. This is really more an issue about appropriate use of super. Anybody using a super call should really understand the contract of the method they think they are calling. If what is being called via super is a constructor that (somethings?) returns a substitute value then the subclass constructor should know that and appropriate capture and use the value returned from the super call.
On 2/3/2014 10:22 AM, Claude Pache wrote:
Therefore, it would be nice if
return expression
would be statically forbidden in theconstructor
method when defined using theclass
syntax (but a barereturn
is fine, which slightly complicates the grammar).
I like the motivation, but doing it as an early error is problematic
(having experienced a validator that differentiates between return
,
return undefined
, and return void expr
and complains loudly).
Instead I would suggest to throw an error at runtime if a class constructor returns anything besides undefined when it is constructed.
-1 to throwing ... Object class is returning something different all the time ... why avoiding the possibility to write facade_ish_ classes ?
How about we keep it explicit as it is now for ES3 ?
Le 3 févr. 2014 à 19:39, Brandon Benvie <bbenvie at mozilla.com> a écrit :
I like the motivation, but doing it as an early error is problematic (having experienced a validator that differentiates between
return
,return undefined
, andreturn void expr
and complains loudly).Instead I would suggest to throw an error at runtime if a class constructor returns anything besides undefined when it is constructed.
I don't see much advantage in being allowed to write return someExpressionThatShouldYieldUndefined
. Indeed, you can always write someExpressionThatShouldYieldUndefined; return
, or even: if (someExpressionThatShouldYieldUndefined !== undefined) throw new TypeError; return
.
Also, the problem, with runtime exception, is that we are no longer able to easily differentiate between construct
methods that are defined using the class
syntax and other construct
methods... which raise the issue of backward compatibility.
We did discuss return expr; in constructors at the TC39 meeting, and my sense is that we want to keep that option. Classes as sugar (mostly), and all that.
This is an esoteric and ugly use case but I'm not trolling. The default constructor for a class which extends another is:
constructor(...args){ super(...args); }
Is there any reason it shouldn't return the value from super?
constructor(...args){ return super(...args); }
Basic constructors still have the quirky behavior of ES functions that they can return any object and don't have to return the instantiated object. This can be useful if they're used as functions or should return a placeholder object, or other instance, for compatibility/legacy reasons. E.g. when you have a custom instantiation process.
class Foo { constructor() { return {}; } }
Currently, this behavior doesn't carry over to subclasses by default:
class Bar extends Foo { }
You'd have to explicitly define a constructor that returns the value from the super call.
Additionally, since DefineMethod is going to be exposed, does it make sense to expose ReferencesSuper too?
Otherwise there is no way to detect, at runtime, if a default constructor function is a top-level constructor (without a super), or a subclass with a super. One of them can use toMethod but not the other.