Skip to content
This repository was archived by the owner on Jan 25, 2022. It is now read-only.

tc39/proposal-class-public-fields

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

33 Commits
 
 

ES Class Properties

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.

Proposal 1/2: Class Instance Properties

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'
  }
}

How It Works

Proposed Syntax

Instance property declarations may either specify an initializer or not:

class ClassWithoutInits {
  myProp;
}

class ClassWithInits {
  myProp = 42;
}
Instance Property Declaration Process

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):

  1. If the property name is computed, evaluate the computed property expression to a string to conclude the name of the property.
  2. 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.
  3. If the DefinedClass.prototype[Symbol.classProperties] object is not already set, create and set it.
  4. 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,

Instance Property Initialization Process

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):

  1. For each entry on DefinedClass.prototype[Symbol.classProperties], call the value as a function with a this value equal to the this value of the object being constructed.
  2. Define the result of the call in step 1 as a property on the this object with a key corresponding to the key of the DefinedClass.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>>,
}

Why?

Expressiveness & Boilerplate

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.

Decorators for Non-Method Class Members

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.

Potential VM Warm-Up Optimizations

When properties are specified declaratively, VMs have an opportunity to generate best-effort member offsets earlier (similar to existing strategies like hidden classes).

Proposal 2/2: Class "Static" Properties

(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'
  }
}

How It Works

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:

  1. If the property name is computed, evaluate the computed property expression to a string to conclude the name of the property.
  2. 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.
  3. If the ClassDefinition[Symbol.classProperties] object is not already set, create and set it.
  4. 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.
  5. Call the function defined in step 2 with a this value equal to the this value of the object being constructed.
  6. Define the result of the call in step 5 as a property on the this object 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.

Why?

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.

Spec Text

ClassPropertyInitializer : 
  PropertyName ;
  PropertyName = AssignmentExpression ;

ClassElement :
  MethodDefinition
  static MethodDefinition
  ClassPropertyInitializer
  static _ClassPropertyInitializer
  ;
(new) 14.5.x Static Semantics: GetDeclaredClassProperties

ClassElementList : ClassElement

  1. If ClassElement is the production ClassElement : ClassPropertyInitializer, return a List containing ClassElement.
  2. If ClassElement is the production ClassElement : static ClassPropertyInitializer, return a list containing ClassElement.
  3. Else return a new empty List.

ClassElementList : ClassElementList ClassElement

  1. Let list be PropertyInitializerList of ClassElementList
  2. If ClassElement is the production ClassElement : ClassPropertyInitializer, append ClassElement to the end of list.
  3. If ClassElement is the production ClassElement : static ClassPropertyInitializer, append ClassElement to the end of list.
  4. Return list.
  1. Let lex be the LexicalEnvironment of the running execution context.
  2. Let classScope be NewDeclarativeEnvironment(lex).
  3. Let classScopeEnvRec be classScope’s environment record.
  4. If className is not undefined, then
    1. Perform classScopeEnvRec.CreateImmutableBinding(className, true).
  5. If ClassHeritageopt is not present, then
    1. Let protoParent be the intrinsic object %ObjectPrototype%.
    2. Let constructorParent be the intrinsic object %FunctionPrototype%.
  6. Else
    1. Set the running execution context’s LexicalEnvironment to classScope.
    2. Let superclass be the result of evaluating ClassHeritage.
    3. Set the running execution context’s LexicalEnvironment to lex.
    4. ReturnIfAbrupt(superclass).
    5. If superclass is null, then
    6. Let protoParent be null.
    7. Let constructorParent be the intrinsic object %FunctionPrototype%.
    8. Else if IsConstructor(superclass) is false, throw a TypeError exception.
    9. Else
    10. If superclass has a [[FunctionKind]] internal slot whose value is "generator", throw a TypeError exception.
    11. Let protoParent be Get(superclass, "prototype").
    12. ReturnIfAbrupt(protoParent).
    13. If Type(protoParent) is neither Object nor Null, throw a TypeError exception.
    14. Let constructorParent be superclass.
  7. Let proto be ObjectCreate(protoParent).
  8. If ClassBodyopt is not present, let constructor be empty.
  9. Else, let constructor be ConstructorMethod of ClassBody.
  10. If constructor is empty, then,
  11. 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.
  12. Else, 1. Let constructor be the result of parsing the String "constructor( ){ }" using the syntactic grammar with the goal symbol MethodDefinition.
  13. Set the running execution context’s LexicalEnvironment to classScope.
  14. Let constructorInfo be the result of performing DefineMethod for constructor with arguments proto and constructorParent as the optional functionPrototype argument.
  15. Assert: constructorInfo is not an abrupt completion.
  16. Let F be constructorInfo.[[closure]]
  17. If ClassHeritageopt is present, set F’s [[ConstructorKind]] internal slot to "derived".
  18. Perform MakeConstructor(F, false, proto).
  19. Perform MakeClassConstructor(F).
  20. Perform CreateMethodProperty(proto, "constructor", F).
  21. If ClassBodyopt is not present, let methods be a new empty List.
  22. Else, let methods be NonConstructorMethodDefinitions of ClassBody.
  23. For each ClassElement m in order from methods
  24. If IsStatic of m is false, then 1. Let status be the result of performing PropertyDefinitionEvaluation for m with arguments proto and false.
  25. Else, 1. Let status be the result of performing PropertyDefinitionEvaluation for m with arguments F and false.
  26. If status is an abrupt completion, then 1. Set the running execution context’s LexicalEnvironment to lex. 2. Return status.
  27. If ClassBodyopt is not present, let propertyDecls be a new empty List.
  28. Else, let propertyDecls be GetDeclaredClassProperties of ClassBody.
  29. For each ClassElement i in order from propertyDecls
  30. let propName be the result of performing PropName of i
  31. 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
  32. Else, 1. Let initFunc be null
  33. 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})
  34. 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
    1. Let propValue be the result of calling initFunc
    2. TODO: Object.defineProperty(F, propName, {configurable: true, enumerable: true, writable: true, value: _propValue})
  35. Set the running execution context’s LexicalEnvironment to lex.
  36. If className is not undefined, then
  37. Perform classScopeEnvRec.InitializeBinding(className, F).
  38. 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:

  1. Assert: F is an ECMAScript function object.
  2. Assert: Type(newTarget) is Object.
  3. Let callerContext be the running execution context.
  4. Let kind be F’s [[ConstructorKind]] internal slot.
  5. If kind is "base", then
  6. Let thisArgument be OrdinaryCreateFromConstructor(newTarget, "%ObjectPrototype%").
  7. ReturnIfAbrupt(thisArgument).
  8. Let calleeContext be PrepareForOrdinaryCall(F, newTarget).
  9. Assert: calleeContext is now the running execution context.\
  10. If kind is "base", then
  11. TODO: Let propInits be the result of GetClassPropertyStore of F.prototype.
  12. 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
    1. TODO: Let propValue be the result of executing propInitFunc with a this of thisArgument.
    2. TODO: Let success be the result of [[Set]](propName, propValue, thisArgument).
    3. TODO: ReturnIfArupt(success)
  13. Perform OrdinaryCallBindThis(F, calleeContext, thisArgument).
  14. Let constructorEnv be the LexicalEnvironment of calleeContext.
  15. Let envRec be constructorEnv’s environment record.
  16. Let result be OrdinaryCallEvaluateBody(F, argumentsList).
  17. Remove calleeContext from the execution context stack and restore callerContext as the running execution context.
  18. If result.[[type]] is return, then
  19. If Type(result.[[value]]) is Object, return NormalCompletion(result.[[value]]).
  20. If kind is "base", return NormalCompletion(thisArgument).
  21. If result.[[value]] is not undefined, throw a TypeError exception.
  22. Else, ReturnIfAbrupt(result).
  23. Return envRec.GetThisBinding().

SuperCall : super Arguments

  1. Let newTarget be GetNewTarget().
  2. If newTarget is undefined, throw a ReferenceError exception.
  3. Let func be GetSuperConstructor().
  4. ReturnIfAbrupt(func).
  5. Let argList be ArgumentListEvaluation of Arguments.
  6. ReturnIfAbrupt(argList).
  7. Let constructResult be Construct(func, argList, newTarget).
  8. ReturnIfAbrupt(constructResult).
  9. Let thisER be GetThisEnvironment( ).
  10. Let bindThisResult thisER.BindThisValue(constructResult).
  11. TODO: Let propInits be the result of GetClassPropertyStore of thisER.[[FunctionObject]].
  12. TODO: For each propInitKeyValuePair from propInits.
  13. Let propName be the first element of propInitKeyValuePair
  14. Let propInitFunc be the second element of propInitKeyValuePair
  15. If propInitFunc is not null, then 1. TODO: Let propValue be the result of executing propInitFunc with a this of thisArgument. 2. TODO: Let success be the result of [[Set]](propName, propValue, thisArgument). 3. TODO: ReturnIfArupt(success)
  16. Return bindThisResult

About

Stage 2 proposal for public class fields in ECMAScript

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Contributors 9