In the currently specced design of classes, the fact that the creation step and the initialisation step of built-in object may be separated by arbitrary user code is thought to be problematic.
Jason proposed a @@new behaviour in replacement of @@create that would avoid the issue [1]. Here is a counter-proposal (or an improved proposal, ad libidum), which is not tightly coupled to @@new. In fact, it is designed to (well, TBH, it happens to) just work in absence of @@create or @@new hook, although it is possible to introduce them. The basic idea is the following: the creation process (the @@create step) is deferred as late as possible. It will appear that, for built-in base classes like Array, creation occurs just before initialisation, making the created-but-initialised state of such objects unobservable, at least in absence of user-overridable @@create hook. Non-Constructed Objects ----------------------- Non-Constructed Objects are introduced in order to describe the state of not-yet-defined this-bindings. Non-Constructed Objects is probably not be the greatest approach to spec the thing, especially when considering the awful Step 3 of InitializeThisBindings() algorithm below; it is just a convenient hack that allows me to expose the idea with minimal change from the current specification draft. A Non-Constructed Object is a special placeholder exotic object with the following internal slots: * [[Constructor]], which holds a reference to the constructor; * [[NonConstructed]], set to `true`. Non-Constructed Objects may only appear as value of this-bindings inside functions (or, using the spec language, as value of the `thisValue` component of a function environment record). Any attempt to get an explicit reference to such an object in user code will throw an error. The only way to pass (implicitely) a reference to a Non-Constructed Object between different function environment records, is through calls to `super`. InitializeThisBindings(nonconstructedObj, obj) abstract operation ---------------------------------------------------------------------- This operation performs the actual initialisation of the this-bindings that were previously deferred: 1. Assert `nonconstructedThisObj` is a Non-Constructed Object. 2. Assert `obj` is an ordinary object. 3. Replace all references to `nonconstructedThisObj` with references to `obj`. (In particular, this step will effectively initialise the this-binding of every function environment record that used to reference `nonconstructedObj`.) Additional runtime semantics of the `this` keyword -------------------------------------------------- Any attempt to get an explicit reference to the `thisValue` of a function environment record while it holds a Non-Constructed Object, shall throw an error. new C(...args) ---------------- When a constructor `C` is called with arguments `args`, the following steps are taken. In particular, the actual initialisation of the this-value is deferred. 1. Let `thisValue` be a new Non-Constructed Object, with its internal slot [[Constructor]] set to `C`. 2. Let `R` be the result of `C`.[[Call]](`thisValue`, `args`). 3. NOTE. The operation InitializeThisBinding() may have been called during the previous step, meaning that `thisValue` may now be a regular object. 4. ReturnIfAbrupt(`R`). 5. If Type(`R`) is Object, return `R`. 6. If `thisValue` is a Non-Constructed Object, throw an error. 7. Return `thisValue`. No more [[Construct]] ---------------------- [[Construct]] internal method is gone. Actually it is conflated with the [[Call]] internal method, modified as below. The key fact is that [[Call]] is able to see if it should have a [[Construct]]-like behaviour, by examining whether its `thisArgument` is a Non-Constructed Object. F.[[Call]] (thisArgument, argumentsList) for user-defined functions ----------------------------------------------------------------------------------- User-defined functions has the currently specced [[Call]] behaviour [Section 9.2.2], with the following additional step inserted somewhere near the beginning of the algorithm, e.g., after step 1: 1bis. If the `thisArgument` is a Non-Constructed Object, a. If `F`’s [[NeedsSuper]] internal slot is set to false (IOW, if `F`’s code doesn’t contain `super`), i. Let `proto` be the result of GetPrototypeFromConstructor(`thisArgument`.[[Constructor]], "%ObjectPrototype%"). ii. ReturnIfAbrupt(`proto`). iii. Let `obj` be ObjectCreate(`proto`). iv. Perform InitializeThisBindings(`thisArgument`, `obj`). v. Assert: Now, `thisArgument` is an ordinary object. b. Else, `thisArgument` is left untouched. // it is meant to be handled at the occasion of the enclosed `super` call. F.[[Call]] (thisArgument, argumentsList) for bound functions ------------------------------------------------------------- In the algorithm sepcced in [Section 9.4.1.1], step 2 is replaced with: 2. If the `thisArgument` is a Non-Constructed Object, let `boundThis` be `thisArgument`. // this is the current [[Construct]] behaviour 2bis. Else, let `boundThis` be the value of `F`’s [[BoundThis]] internal slot. // this is the current [[Call]] behaviour F.[[Call]] (thisArgument, argumentsList) for the built-in Object constructor ----------------------------------------------------------------------------- There is no change: `Object(...)` acts as a factory rather than as a constructor, as currently specified, and the `thisArgument` is ignored. In particular trying to subclass `Object` will lead to unexpected results. Note however that `new Object` does still work. F.[[Call]] (thisArgument, argumentsList) for the built-in Array contsructor --------------------------------------------------------------------------- The [[ArrayInitializationState]] internal slot is gone, and Step 4 of the algorithms in [Sections 22.1.1.*] is replaced with (where `O` is the this-value): 4. If `O` is a Non-Constructed Object, a. Let `proto` be the result of GetPrototypeFromConstructor(`O`.[[Constructor]], "%ArrayPrototype%"). b. ReturnIfAbrupt(`proto`) a. Let `array` be ArrayCreate(<<length>>, `proto`). b. Perform InitializeThisBindings(`thisArgument`, `array`) 5. Else, etc. The [[Call]] behaviour of other built-in constructors is left as an exercise to the reader. Comments -------- There is a nice side-effect of the proposal: The new internal check intended to discriminate between call-as-function and call-as-constructor is easier and more robust. In particular, * hacks such as [[ArrayInitializationState]] are no longer needed; * bound functions are truly subclassable (see [2]). However, it remains very hard for user-defined functions to distinguish correctly between constructor/initialisation-calls and method/function-calls, or to write code that works well in both cases. (At least the situation is not worse than in ES5-.) Optional: the @@create hook ---------------------------- A @@create hook can easily be placed as follows: In each [[Call]] internal methods defined above, a call to (`thisArgument`.[[Constructor]]).@@create could replace the steps spanning from GetPrototypeFromConstructor(...) inclusive to InitializeThisBindings(...) exclusive. Whether such a hook is compatible with, e.g., DOM constructors, is left to the appreciation of the competent people. At worst, a built-in constructor could cheat by defining its own [[Call]] internal method that would refuse to run the @@create hook. Optional: the @@new hook ------------------------- Alternatively, the following hook may be installed: At the beginning of each call to F.[[Call]], the following steps are taken: 1. if `thisArgument` is a Non-Constructed Object, a. If `F` has an *own* property named @@new, i. Let `R` be the result of `F`[@@new].[[Call]](`thisArgument`.[[Constructor]], `argumentsList`). ii. ReturnIfAbrupt(`R`). iii. If Type(`R`) is Object, α. InitializeThisBindings(`thisArgument`, `R`). iv. Return `R`. b. etc. 2. etc. Note that we don’t look for inherited @@new property, in order to preserve the initialise-at-latest-time behaviour. —Claude [Section 9.2.2]: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-ecmascript-function-objects-call-thisargument-argumentslist [Section 9.4.1.1]: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-call [Sections 22.1.1.*]: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-array-constructor [1]: http://esdiscuss.org/topic/new [2]: http://esdiscuss.org/topic/issue-when-subclassing-a-bound-function-used-as-constructor _______________________________________________ es-discuss mailing list es-discuss@mozilla.org https://mail.mozilla.org/listinfo/es-discuss