This presents two related proposals: "class instance fields" and "class static properties". "Class instance fields" describe properties intended to exist on instances of a class (and may optionally include initializer expressions for said properties). "Class static properties" are declarative properties that exist on the class object itself (and may optionally include initializer expressions for said properties).
The first part of this proposal is to include a means of declaring fields for instances of an ES class. These field 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 field declarations may either specify an initializer or not:
class ClassWithoutInits {
myProp;
}
class ClassWithInits {
myProp = 42;
}
When an instance field is specified with no initializer, the presence of the field declaration will have the effect of doing Object.defineProperty(this, propName, { [...] value: this.prop})
on the constructed object (potentially reading through a prototype chain on the RHS via [[Get]]). The ability to leave off an initializer is important 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). And the reason we copy the [[Get]] result into an own-property is to ensure that any changes to objects that sit in the prototype chain (such as, perhaps, a method by the same name) do not change the declared-field properties on the instance.
For example:
class Base {
initialized() { return 'base initialized'; }
uninitialized() { return 'base uninitialized'; }
}
class Child extends Base {
initialized = () => 'child initialized';
uninitialized;
}
var child = new Child();
child.initialized(); // 'child initialized'
child.uninitialized(); // 'base uninitialized'
Base.prototype.initialized = () => 'new base initialized';
Base.prototype.uninitialized = () => 'new base uninitialized';
child.initialized(); // Remains 'child initialized'
child.uninitialized(); // Remains 'base uninitialized'
Additionally, it's sometimes useful for derived classes to "silently" specify a class field that may have been setup on a base class (perhaps using field declarations). For this reason, writing null
or undefined
and potentially overwriting data written by a parent class cannot happen.
When an instance field with an initializer is specifed on a non-derived class (AKA a class without an extends
clause), the list of fields (each comprised of a name and an initializer expression) are stored in a slot on the class object 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 an instance field with an initializer is specified on a derived class (AKA a class with an extends
clause), the list of fields (each comprised of a name and an initializer expression) are stored in a slot on the class object 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 fields specified on the derived class will not be initialized since property initialization is considered a part of the SuperCall Evaluation process.
During the process of executing field initializers at construction time, field initializations happen in the order the fields were declared on the class.
The process of declaring instance fields on a class happens at the time of class definition evaluation. This process is roughly defined as follows for each field in the order the fields are declared:
- Let F be the class object being defined.
- Let fieldName be the result of evaluating the field name expression (i.e. potentially a computed property).
- If an initializer expression is present on the field declaration
- Let fieldInitializerExpression be a thunk of the initializer expression.
- Else,
- Let fieldInitializerExpression be a thunk of the expression represented by [[Get]](fieldName, this)
- Call F.[[SetClassInstanceField]](fieldName, fieldInitializerExpression) to store the field name + initializer on the constructor.
Note that the purpose for storing the initializer expressions as a thunk is to allow the deferred execution of the initialization expression until class construction time; Thus,
The process for executing a field initializer happens at class instantiation time. The following describes the process for initializing each class field initializer (intended to run once for each field in the order they are declared):
- Let instance be the object being instantiated.
- Let fieldName be the name for the current field (as stored in the slot on the constructor function).
- Let fieldInitializerExpression be the thunked initializer expression for the current field (as stored in the slot on the constructor function).
- Let initializerResult be the result of evaluating fieldInitializerExpression with
this
equal to instance. - Let propertyDescriptor be PropertyDescriptor{[[Value]]: initializerResult, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: false}.
- Call instance.[[DefineOwnProperty]](fieldName, propertyDescriptor).
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. One must assign to an expando property on this
in the constructor -- or anywhere, really. This poses an inconvenience to tooling (and also often 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 instance-generated 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 field declarations it becomes possible for tools, documentation, and runtimes 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 an override constructor, call super()
, etc).
Initialization situations like the following are common in many pervasive frameworks like React, Ember, Backbone, etc. as well as "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, class instance fields must also provide a syntactic (and semantic) space for specifying decorators on said fields. 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 class instance fields are specified declaratively, VMs have an opportunity to generate best-effort optimizations earlier (at compile time) similar to existing strategies like hidden classes.
(This is very much related to the former section, but is much simpler in scope and is technically orthogonal -- so I've separated it for simplicity)
This second part of the proposal intends to include a declarative means of expressing "static" properties on an ES class object. 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:
- Let F be the class object being defined.
- Let propName be the result of executing PropName of the PropertyName of the static property declaration
- If an initializer expression is present
- Let initializerValue be the result of evaluating the initializer expression.
- Else,
- Let initializerValue be the result of [[Get]](propName, F)
- Let propertyDescriptor be PropertyDescriptor{[[Value]]: initializerValue, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: false}.
- Call F.[[DefineOwnProperty]](propName, propertyDescriptor).
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.
(TODO: Spec text is outdated and is pending updates to changes that were made since the September2015 TC39 meeting and are already reflected in the above abstracts)
ClassFieldInitializer :
PropertyName ;
PropertyName = AssignmentExpression ;
ClassElement :
MethodDefinition
static MethodDefinition
ClassFieldInitializer
static ClassFieldInitializer
;
ClassElementList : ClassElement
- If ClassElement is the production ClassElement : ClassPropertyInitializer, return a List containing ClassElement.
- If ClassElement is the production ClassElement :
static
ClassPropertyInitializer, 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 :
static
ClassPropertyInitializer, 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
- Let constructor be the result of parsing the String "constructor(... args){ super (...args);}" using the syntactic grammar with the goal symbol MethodDefinition.
- Else,
- Let constructor be the result of parsing the String "constructor( ){ }" using the syntactic grammar with the goal symbol MethodDefinition.
- If ClassHeritageopt is present, then
- 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
- Let status be the result of performing PropertyDefinitionEvaluation for m with arguments proto and false.
- Else,
- Let status be the result of performing PropertyDefinitionEvaluation for m with arguments F and false.
- If status is an abrupt completion, then
- Set the running execution context’s LexicalEnvironment to lex.
- Return status.
- If IsStatic of m is false, then
- 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
- 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,
- Let initFunc be null
- If IsStatic of i is false, then
- TODO: Let propertyStore be GetClassPropertyStore of proto
- TODO: Object.defineProperty(propertyStore, propName, {configurable: false, enumerable: true, writable: true, value: initFunc})
- Else,
- TODO: Let propertyStore be GetClassPropertyStore of F
- TODO: Object.defineProperty(propertyStore, propName, {configurable: false, enumerable: true, writable: true, value: initFunc})
- TODO: If HasRHSInitializer of i is true, then
- Let propValue be the result of calling initFunc
- TODO: Object.defineProperty(F, propName, {configurable: false, 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.
- Let propName be the first element of propInitKeyValuePair
- Let propInitFunc be the second element of propInitKeyValuePair
- If propInitFunc is not
null
, then- TODO: Let propValue be the result of executing propInitFunc with a
this
of 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- TODO: Let propValue be the result of executing propInitFunc with a
this
of 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
- Return bindThisResult