This article summarizes the current and proposed class fields and methods semantics. With the exception of private static fields and methods at the time of writing, all the described semantics enjoy current TC39 consensus.
I will talk about the following table of features. The full feature set is the product of the columns.
Visibility
Placement
Class Element
Public
Instance
Field
Private
Static
Method
Visibility
Public
Private
Placement
Instance
Static
Class Element
Field
Method
I will describe the semantics of each column in detail, as well as the consequences of their combinations with each other and with existing JS language features.
Public instance and static methods are already shipping with ES6. The rest of the above feature matrix is being advanced by 3 individual proposals:
Public instance fields and public static fields are shipping in Chrome 72. Implementation work for fields is ongoing in Firefox and Safari.
Mental Model
Here is the mental model for the columns above. These intuitions will be bolstered by a deep dive into the semantics with concrete examples in the next section.
Visibility
A public element is a writable and configurable property. As a property, public things participate in prototype inheritance.
A private element is that which is accessible via lexically scoped names only on objects of distinguished provenance. Since they are not properties, they also do not participate in prototype inheritance.
Thought of another way, the behavior of private is a strict subset of the behavior of public. Encapsulation is enabled precisely by combination of lexical scoping with the provenance restriction on object dispatch. The JS concept of private is designed for JS and differs from “private” in other languages.
Placement
An instance element is accessed on instances of a class.
A static element is accessed on class constructors.
Class Element
A field is state associated with a class that has a blessed declarative syntax. An instance field is per-instance state. A static field is per-class constructor state.
A method is some behavior associated with a class that has a blessed declarative syntax. Both instance and static methods have one identity per-class evaluation. Instance methods are on the prototype, and static methods are on the class constructor.
Let’s Make the Semantics Concrete with Examples
I hope to make clear the semantics of all class elements below with examples. Each example will be followed by a short explanation of the highlighted semantics.
Public instance fields
Classes may declare public fields accessible as properties on instances.
Public instance fields are properties added by Object.defineProperty. They are added at construction time of the object for the base class, before the constructor body runs.
For subclasses, this throws ReferenceError if touched until super() is called.(*) So, public instance fields are added when super() returns.
All constructors in JavaScript can return a different object, overriding the result from new and the newly bound this value from super(). For instance fields, if the super constructor returns something different, fields from the subclass are still added.
Public field names may be computed, like properties. They are evaluated once per class evaluation.
Public instance methods
Classes may declare public methods accessible on instances via the prototype.
Public instance methods are added to the class prototype with Object.defineProperty at class evaluation time. They are writable, non-enumerable, and configurable.
Generator function, async function, and async generator function forms may also be public instance methods.
Getters and setters are possible as well. There are no generator, async, or async generator getter and setter forms.
In instance methods, super references the superclass’s prototype property. The following invokes Base.prototype.basePublicMethod.(†)
Private instance fields
Classes may declare private fields accessible on base class or subclass instances from inside the lexical scope of the class declaration itself.
Private instance fields are declared with # names (said “hash names”), identifiers that are prefixed with #. Though a different character, this follows the convention of signaling privacy with _-prefixed property names.
# is the new _,
with encapsulation being enforced by the language instead of by convention. # is part of the name itself and is used in both in declaration and access.
The lexical scoping rules of # names are stricter than those of identifier names. It is a syntax error to refer to # names that are not in scope.
Like lexical bindings, it is a syntax error to have multiple same-named # names in the same scope (i.e. the class declaration), while shadowing via nested scopes is allowed.
Note that since all # names start with # and property names cannot start with #, the two cannot be in conflict.
It is also a syntax error to delete# names.
Private field accesses are strictly more restrictive than public field accesses. Getting a # name on an object that doesn’t have a private field with that name is a TypeError. The same goes for setting a # name. Private fields are not properties, and do not participate in the machinery of property lookup like prototype inheritance. This is to enable encapsulation, as property lookup machinery is observable, e.g., by Proxy.
Private fields combines lexical scoping with a provenance restriction on dispatch. For private instance fields, the provenance is having been constructed, either as a base class or a subclass, by the class that declared the private instance field.
# names are accessible through direct eval, like other lexically scoped things.
Private fields are added at the same time as public fields, either at construction time in the base class, or after super() returns in a subclass.
Like with public instance fields, if the super() overrides the return value, private fields from the subclass are still added. For implementers, this means private fields may be added to arbitrary objects.
While # names are not first class in the language, they have observably distinct per-class evaluation identities. One evaluation of a class declaration cannot access the private fields of another evaluation of the same declaration.
Finally, note that there is currently no shorthand notation for accessing # names. Their access requires a receiver.
Private instance methods
Private instance methods are analogous to public instance methods. Their access is restricted in the same fashion as private instance fields.
These methods are specified as non-writable private fields of class instances. Like private instance fields, they are added at construction time for the base classes and after super() returns for subclasses.
Generator function, async function, and async generator function forms may also be private instance methods.
Getters and setters are possible as well. There are no generator, async, or async generator getter and setter forms for private instance methods.
Private methods are specified as a per-instance list of map of # names to property descriptors. This preserves the symmetry of expressible forms with public instance methods, as well as enforce the restrictions that come with private fields.
There is a single function identity per class-evaluation for private instance methods. Even though they are specified as per-instance private fields, instance methods are shared across all instances.
In private instance methods, super and this follow the same semantics as those in public instance methods. Since private fields and methods are not added to the prototype, #privateMethod() throws below. Similarly, when an object of incompatible provenance is passed as the receiver to an instance private method, the private field lookup throws.
Instance field initializers
All fields can be initialized with an initializer expression in situ with the declaration. Initializers are run in declaration order. Instance field initializer expressions are specified as the bodies of non-observable instance methods, which informs the values of this, new.target, and super.
Fields without initializers are initialized to undefined.
Field initializers are run as fields are added, in declaration order, and this order is observable by the initializers. #privateField is false in the following as publicField has has not been added yet when evaluating #privateField’s initializer. There is also no error when initializing publicField with this.#privateField, owing to its coming after #privateField.
These initializers are specified as methods that return the result of the initializer expressions. The methods need not be reified in implementation, but this fiction of specification informs the values of this. In instance field initializers, this is the object under construction. By the time the instance field initializer runs, this is accessible.
Multiple same-named public fields and methods are allowed in the same class declaration, and their initializers are run in order. Since public methods and fields are properties added with Object.defineProperty, the last field or method overrides all previous ones.
This does not happen with private fields and methods, as multiple same-named # names in the same scope is a syntax error.
Instance methods are added before any initializer is run, so all instance methods are available in instance field initializers.
new.target is undefined in field initializers.
As in instance methods, super references the superclass’s prototype in instance field initializers.
Class bodies are always strict code, so initializers cannot leak any new bindings via direct eval.
Use of arguments is a syntax error in field initializers.
Public static fields
Classes may declare public static fields, which are accessible as properties on the class constructor.
Public static fields are added to the class constructor with Object.defineProperty at class evaluation time. Their semantics are otherwise identical to public instance fields.
Public static fields are only initialized on the class in which they are defined, not reinitialized on subclasses. Subclass constructors have their superclasses as their prototype. Public static fields of superclasses are accessed via the prototype chain.
Public static methods
Public static methods are declared function forms and, like public static fields, are also accessible as properties on the class constructor. Also like public instance methods, generator function, async function, async generator function, getter, and setter forms are accepted.
These methods are added to the class constructor with Object.defineProperty at class evaluation time. Like public instance methods, they are writable, non-enumerable, and configurable.
In static methods, super references the superclass constructor, and the superclass’s public static methods are accessible.(‡)
Private static fields
Classes may declare private fields accessible on the class constructor from inside the lexical scope of the class declaration itself.
Private static fields are added to the class constructor at class evaluation time.
The provenance restriction of private static fields restricts access to the class constructor. The following throws because the this value of basePublicStaticMethod is the subclass constructor, which does not have the #BASE_PRIVATE_STATIC_FIELD field. This is the natural result, though unexpected for some, of the composition of the private field provenance restriction with this dynamicity.
This type of error can be avoided by always using the class constructor as the receiver when accessing private static elements.
Private static methods
Private static methods are analogous to public static methods. Their access is restricted in the same fashion as private static fields.
These methods are specified as non-writable private fields of class constructors. Like private static fields, they are added at class evaluation time.
Like all methods, generator function, async function, async generator function, getter, and setter forms are accepted.
Like public static methods, super references the superclass constructor.
Static field initializers
Static fields may have in-situ initializers. Like instance field initializers, they are also run in declaration order, and the expressions are specified as bodies of non-observable static methods.
Like instance field initializers, static methods are added before any initializer is run, so all static methods are available in static field initializers.
As initializer expressions are specified as static method bodies, super references the superclass constructor, and this references the class constructor.
By the time static field initializers are evaluated, the class name binding inside the class scope is initialized (i.e. may be accessed and does not throw a ReferenceError).
Some Take Aways
We’ve explored the semantics of all JavaScript class elements. Perhaps some semantics were surprising, while others were expected. I believe a common source of mismatched intuition for private fields is the provenance restriction on # names, and I hope this article was helpful in making this clearer. In closing, I offer these two aphorisms.
# is the new _
and
private fields have a provenance restriction
Acknowledgments
I would like to thank Dan Ehrenberg for tireless championing of the class features and help editing this article, and Rob Palmer for proofing and suggestions.
Edits
2 May, 2018 — Corrected example 38; reformatted to read better on mobile.
3 May, 2018 — Added this semantics to example 39.
4 May, 2018 — Corrected example 3. (From Thomas Chetwin)
6 November, 2018 — Updated static fields proposal from stage 2 to stage 3.
1 February, 2019 — Corrected example 15. (From @WomanCorn)
Specification Footnotes
(*) In subclass constructors, this is in the TDZ (temporal dead zone) until super() returns.
(†) In instance methods, [[HomeObject]] is the class prototype.
(‡) In static methods, [[HomeObject]] is the class constructor.