This presents two related proposals: "class instance" property initializers and "class static" property intializers. "Instance" properties exist once per instiation of a class on the this value, and "static" properties exist on the class object itself.
This is a proposal to include a declarative means of expressing instance properties on an ES class. These property declarations may include intializers, but are not required to do so.
The proposed syntax for this is as follows:
class MyClass {
myProp = 42;
constructor() {
console.log(this.myProp); // Prints '42'
}
}Instance property declarations may either specify an initializer or not:
class ClassWithoutInits {
myProp;
}
class ClassWithInits {
myProp = 42;
}When a property is specified with no initializer, the presence of the property will have no effect on any objects instantiated from the class. This is useful for scenarios where initialization needs to happen somewhere other than in the declarative initialization position (ex. If the property depends on constructor-injected data and thus needs to be initialized inside the construtor, or if the property is managed externally by something like a decorator or framework).
Additionally, it's sometimes useful for derived classes to "silently" specify a class property that may have been setup on a base class (either using or not using property declarations). For this reason, a declaration with no initializer should not attempt to overwrite data potentially written by a base class.
When a property with an initializer is specifed on a non-derived class (AKA a class without an extends clause), the initializers are declared and executed in the order they are specified in the class definition. Execution of the initializers happens during the internal "initialization" process that occurs immediately before entering the constructor.
When a property with an initializer is specified on a derived class (AKA a class with an extends clause), the initializers are declared and executed in the order they are specified in the class definition. Execution of the initializers happens at the end of the internal "initialization" process that occurs while executing super() in the derived constructor. This means that if a derived constructor never calls super(), instance properties specified on the derived class will not be initialized since property initialization is considered a part of the SuperCall Evaluation process.
The process of declaring a property happens at the time of class definition evaluation. This process is roughly defined as follows for each property in the order the properties are declared. (for sake of definition we assume a name for the class being defined is DefinedClass):
- If the property name is computed, evaluate the computed property expression to a string to conclude the name of the property.
- Create a function whose body simply executes the initializer expression and returns the result. This function's parent scope should be set to the scope of the class body. To be super clear: This scope should sit sibling to the scope of any of the class's method bodies.
- If the
DefinedClass.prototype[Symbol.classProperties]object is not already set, create and set it. - On the
DefinedClass.prototype[Symbol.classProperties]object, store the function generated in step 2 under the key matching the name of the property being evaluated.
Note that declared instance properties are wrapped in a function and stored on DefinedClass.prototype[Symbol.classProperties] for purposes of userland introspection. This also means that it is possible to modify a previously declared class's properties by modifying DefinedClass.prototype[Symbol.classProperties] in userland. The ability to introspect and interact with a class definition is important for meta-programming libraries including some frameworks and testing utilities.
The purpose for generating and storing these "thunk" functions is a means of deferring the execution of the initialization expression until the class is constructed; Thus,
The process for executing a property initializer happens at class instantiation time. The following describes the process for initializing each class property initializer (intended to run once for each property in the order the properties are declared):
- For each entry on
DefinedClass.prototype[Symbol.classProperties], call the value as a function with athisvalue equal to thethisvalue of the object being constructed. - Define the result of the call in step 1 as a property on the
thisobject with a key corresponding to the key of theDefinedClass.prototype[Symbol.classProperties]entry currently being evaluated. It should be defined with the following descriptor:
{
configurable: true,
enumerable: true,
writable: true,
get: undefined,
set: undefined,
value: <<initializer result from step 1>>,
}The current idiomatic means of initializing a property on a class instance does not provide an expressively distinct way to "declare" them as part of the structure of a class. In order to create a class property today one must assign to an expando property on this in the constructor -- or anywhere, really. This poses an inconvenience to tooling (and also sometimes humans) when trying to deduce the intended set of members for a class simply because there is no clear distinction between initialization logic and the intended shape of the class.
Additionally, because properties often need to be setup during class construction for object initialization, derived classes that wish to declare/initialize their own properties must implement some boilerplate to execute base class initialization first:
class ReactCounter extends React.Component {
constructor(props) { // boilerplate
super(props); // boilerplate
// Setup initial "state" property
this.state = {
count: 0
};
}
}By allowing explicit and syntactically distinct property declarations, it becomes possible for tools and documentation to easily extract the intended shape of a class and it's objects. Additionally it becomes possible for derived classes to specify non-constructor-dependent property initialization without having to explicitly intercept the constructor chain (write a constructor, call super(), etc).
Initialization situations like the following are common in many pervasive frameworks like React, Ember, Backbone, etc. as well as even just "vanilla" application code:
class ReactCounter extends React.Component {
// Freshly-built counters should always start at zero!
state = {
count: 0
};
}Additionally, static analysis tools like Flow, TypeScript, ESLint, and many others can take advantage of the explicit declarations (along with additional metadata like typehints or JSDoc pragmas) to warn about typos or mistakes in code if the user declaratively calls out the shape of the class.
In lockstep with the sibling proposal for class-member decorators, declarative class properties also provide a syntactic (and semantic) space for specifying decorators on class properties. This opens up an expansive set of use cases for decorators within classes beyond what could otherwise only be applied to method members. Some examples include @readonly (for, say, specifying writable:false on the property descriptor), or @hasMany (for systems like Ember where the framework may generate a getter that does a batched fetch), etc.
When properties are specified declaratively, VMs have an opportunity to generate best-effort member offsets earlier (similar to existing strategies like hidden classes).
(This is a proposal very much related to the former, but is much simpler in scope and is technically orthogonal -- so I've separated it for simplicity.)
This second proposal intends to include a declarative means of expressing "static" properties on an ES class. These property declarations may include intializers, but are not required to do so.
The proposed syntax for this is as follows:
class MyClass {
static myStaticProp = 42;
constructor() {
console.log(MyClass.myStaticProp); // Prints '42'
}
}Static property declarations are fairly straightforward in terms of semantics compared to their instance-property counter-parts. When a class definition is evaluated, the following set of operations is executed:
- If the property name is computed, evaluate the computed property expression to a string to conclude the name of the property.
- Create a function whose body simply executes the initializer expression and returns the result. This function's parent scope should be set to the scope of the class body. To be super clear: This scope should sit sibling to the scope of any of the class's method bodies.
- If the
ClassDefinition[Symbol.classProperties]object is not already set, create and set it. - On the
ClassDefinition[Symbol.classProperties]object, store the function generated in step 2 under the key matching the same name of the property being evauated. - Call the function defined in step 2 with a
thisvalue equal to thethisvalue of the object being constructed. - Define the result of the call in step 5 as a property on the
thisobject with a key corresponding to the name of the property currently being evaluated. It should be defined with the following descriptor:
{
configurable: true,
enumerable: true,
writable: true,
get: undefined,
set: undefined,
value: <<initializer result from step 1>>,
}Note that we store the static property thunk functions on ClassDefinition[Symbol.classProperties] for purposes of userland reflection on how the class was declared.
Currently it's possible to express static methods on a class definition, but it is not possible to declaratively express static properties. As a result people generally have to assign static properties on a class after the class declaration -- which makes it very easy to miss the assignment as it does not appear as part of the definition.
ClassPropertyInitializer :
PropertyName ;
PropertyName = AssignmentExpression ;
ClassElement :
MethodDefinition
static MethodDefinition
ClassPropertyInitializer
static _ClassPropertyInitializer
;
ClassElementList : ClassElement
- If ClassElement is the production ClassElement : ClassPropertyInitializer, return a List containing ClassElement.
- If ClassElement is the production ClassElement :
staticClassPropertyInitializer, return a list containing ClassElement. - Else return a new empty List.
ClassElementList : ClassElementList ClassElement
- Let list be PropertyInitializerList of ClassElementList
- If ClassElement is the production ClassElement : ClassPropertyInitializer, append ClassElement to the end of list.
- If ClassElement is the production ClassElement :
staticClassPropertyInitializer, append ClassElement to the end of list. - Return list.
- Let lex be the LexicalEnvironment of the running execution context.
- Let classScope be NewDeclarativeEnvironment(lex).
- Let classScopeEnvRec be classScope’s environment record.
- If className is not undefined, then
- Perform classScopeEnvRec.CreateImmutableBinding(className, true).
- If ClassHeritageopt is not present, then
- Let protoParent be the intrinsic object %ObjectPrototype%.
- Let constructorParent be the intrinsic object %FunctionPrototype%.
- Else
- Set the running execution context’s LexicalEnvironment to classScope.
- Let superclass be the result of evaluating ClassHeritage.
- Set the running execution context’s LexicalEnvironment to lex.
- ReturnIfAbrupt(superclass).
- If superclass is null, then
- Let protoParent be null.
- Let constructorParent be the intrinsic object %FunctionPrototype%.
- Else if IsConstructor(superclass) is false, throw a TypeError exception.
- Else
- If superclass has a [[FunctionKind]] internal slot whose value is "generator", throw a TypeError exception.
- Let protoParent be Get(superclass, "prototype").
- ReturnIfAbrupt(protoParent).
- If Type(protoParent) is neither Object nor Null, throw a TypeError exception.
- Let constructorParent be superclass.
- Let proto be ObjectCreate(protoParent).
- If ClassBodyopt is not present, let constructor be empty.
- Else, let constructor be ConstructorMethod of ClassBody.
- If constructor is empty, then,
- If ClassHeritageopt is present, then 1. Let constructor be the result of parsing the String "constructor(... args){ super (...args);}" using the syntactic grammar with the goal symbol MethodDefinition.
- Else, 1. Let constructor be the result of parsing the String "constructor( ){ }" using the syntactic grammar with the goal symbol MethodDefinition.
- Set the running execution context’s LexicalEnvironment to classScope.
- Let constructorInfo be the result of performing DefineMethod for constructor with arguments proto and constructorParent as the optional functionPrototype argument.
- Assert: constructorInfo is not an abrupt completion.
- Let F be constructorInfo.[[closure]]
- If ClassHeritageopt is present, set F’s [[ConstructorKind]] internal slot to "derived".
- Perform MakeConstructor(F, false, proto).
- Perform MakeClassConstructor(F).
- Perform CreateMethodProperty(proto, "constructor", F).
- If ClassBodyopt is not present, let methods be a new empty List.
- Else, let methods be NonConstructorMethodDefinitions of ClassBody.
- For each ClassElement m in order from methods
- If IsStatic of m is false, then 1. Let status be the result of performing PropertyDefinitionEvaluation for m with arguments proto and false.
- Else, 1. Let status be the result of performing PropertyDefinitionEvaluation for m with arguments F and false.
- If status is an abrupt completion, then 1. Set the running execution context’s LexicalEnvironment to lex. 2. Return status.
- If ClassBodyopt is not present, let propertyDecls be a new empty List.
- Else, let propertyDecls be GetDeclaredClassProperties of ClassBody.
- For each ClassElement i in order from propertyDecls
- let propName be the result of performing PropName of i
- TODO: If HasRHSExpression of i, then 1. TODO: Let initFunc be a function with an outer environment set to that of the class body that returns the result of executing the RHS expression
- Else, 1. Let initFunc be null
- If IsStatic of i is false, then 1. TODO: Let propertyStore be GetClassPropertyStore of proto 2. TODO: Object.defineProperty(propertyStore, propName, {configurable: true, enumerable: true, writable: true, value: initFunc})
- Else,
1. TODO: Let propertyStore be GetClassPropertyStore of F
2. TODO: Object.defineProperty(propertyStore, propName, {configurable: true, enumerable: true, writable: true, value: initFunc})
3. TODO: If HasRHSInitializer of i is true, then
- Let propValue be the result of calling initFunc
- TODO: Object.defineProperty(F, propName, {configurable: true, enumerable: true, writable: true, value: _propValue})
- Set the running execution context’s LexicalEnvironment to lex.
- If className is not undefined, then
- Perform classScopeEnvRec.InitializeBinding(className, F).
- Return F.
The [[Construct]] internal method for an ECMAScript Function object F is called with parameters argumentsList and newTarget. argumentsList is a possibly empty List of ECMAScript language values. The following steps are taken:
- Assert: F is an ECMAScript function object.
- Assert: Type(newTarget) is Object.
- Let callerContext be the running execution context.
- Let kind be F’s [[ConstructorKind]] internal slot.
- If kind is "base", then
- Let thisArgument be OrdinaryCreateFromConstructor(newTarget, "%ObjectPrototype%").
- ReturnIfAbrupt(thisArgument).
- Let calleeContext be PrepareForOrdinaryCall(F, newTarget).
- Assert: calleeContext is now the running execution context.\
- If kind is "base", then
- TODO: Let propInits be the result of GetClassPropertyStore of F.prototype.
- TODO: For each propInitKeyValuePair from propInits.
1. Let propName be the first element of propInitKeyValuePair
2. Let propInitFunc be the second element of propInitKeyValuePair
2. If propInitFunc is not
null, then- TODO: Let propValue be the result of executing propInitFunc with a
thisof thisArgument. - TODO: Let success be the result of [[Set]](propName, propValue, thisArgument).
- TODO: ReturnIfArupt(success)
- TODO: Let propValue be the result of executing propInitFunc with a
- Perform OrdinaryCallBindThis(F, calleeContext, thisArgument).
- Let constructorEnv be the LexicalEnvironment of calleeContext.
- Let envRec be constructorEnv’s environment record.
- Let result be OrdinaryCallEvaluateBody(F, argumentsList).
- Remove calleeContext from the execution context stack and restore callerContext as the running execution context.
- If result.[[type]] is return, then
- If Type(result.[[value]]) is Object, return NormalCompletion(result.[[value]]).
- If kind is "base", return NormalCompletion(thisArgument).
- If result.[[value]] is not undefined, throw a TypeError exception.
- Else, ReturnIfAbrupt(result).
- Return envRec.GetThisBinding().
SuperCall : super Arguments
- Let newTarget be GetNewTarget().
- If newTarget is undefined, throw a ReferenceError exception.
- Let func be GetSuperConstructor().
- ReturnIfAbrupt(func).
- Let argList be ArgumentListEvaluation of Arguments.
- ReturnIfAbrupt(argList).
- Let constructResult be Construct(func, argList, newTarget).
- ReturnIfAbrupt(constructResult).
- Let thisER be GetThisEnvironment( ).
- Let bindThisResult thisER.BindThisValue(constructResult).
- TODO: Let propInits be the result of GetClassPropertyStore of thisER.[[FunctionObject]].
- TODO: For each propInitKeyValuePair from propInits.
- Let propName be the first element of propInitKeyValuePair
- Let propInitFunc be the second element of propInitKeyValuePair
- If propInitFunc is not
null, then 1. TODO: Let propValue be the result of executing propInitFunc with athisof thisArgument. 2. TODO: Let success be the result of [[Set]](propName, propValue, thisArgument). 3. TODO: ReturnIfArupt(success) - Return bindThisResult