Daniel Ehrenberg, Jeff Morrison
To define a counter widget which increments when clicked, you can define the following with ES2015:
class Counter extends HTMLElement {
clicked() {
this.x++;
window.requestAnimationFrame(this.render.bind(this));
}
constructor() {
super();
this.onclick = this.clicked.bind(this);
this.x = 0;
}
connectedCallback() { this.render(); }
render() {
this.textContent = this.x.toString();
}
}
window.customElements.define('num-counter', Counter);
With the ESnext field declarations proposal, the above example can be written as
class Counter extends HTMLElement {
x = 0;
clicked() {
this.x++;
window.requestAnimationFrame(this.render.bind(this));
}
constructor() {
super();
this.onclick = this.clicked.bind(this);
}
connectedCallback() { this.render(); }
render() {
this.textContent = this.x.toString();
}
}
window.customElements.define('num-counter', Counter);
In the above example, you can see a field declared with the syntax x = 0
. You can also declare a field without an initializer as x
. By declaring fields up-front, class definitions become more self-documenting; instances go through fewer state transitions, as declared fields are always present.
The above example has some implementation details exposed to the world that might be better kept internal. Using ESnext private fields and methods, the definition can be refined to:
class Counter extends HTMLElement {
#x = 0;
clicked() {
this.#x++;
window.requestAnimationFrame(this.render.bind(this));
}
constructor() {
super();
this.onclick = this.clicked.bind(this);
}
connectedCallback() { this.render(); }
render() {
this.textContent = this.#x.toString();
}
}
window.customElements.define('num-counter', Counter);
To make fields private, just give them a name starting with #
.
By defining things which are not visible outside of the class, ESnext provides stronger encapsulation, ensuring that your classes' users don't accidentally trip themselves up by depending on internals, which may change version to version.
Note that ESnext provides private fields only as declared up-front in a field declaration; private fields cannot be created later, ad-hoc, through assigning to them, the way that normal properties can.
A public field declarations define fields on instances with the internals of Object.defineProperty
(which we refer to in TC39 jargon as [[Define]]
semantics), rather than with this.field = value;
(referred to as [[Set]]
semantics). Here's an example of the impact:
class A {
set x(value) { console.log(value); }
}
class B extends A {
x = 1;
}
With the adopted semantics, new B()
will result in an object which has a property x
with the value 1
, and nothing will be written to the console. With the alternate [[Set]]
semantics, 1
would be written to the console, and attempts to access the property would lead to a TypeError
(because the getter is missing).
The choice between [[Set]]
and [[Define]]
is a design decision contrasting different kinds of expectations of behavior: Expectations that the field will be created as a data property regardless of what the superclass contains, vs expectations that the setter would be called. Following a lengthy discussion, TC39 settled on [[Define]]
semantics, finding that it's important to preserve the first expectation.
The decision to base public field semantics on Object.defineProperty
was based on extensive discussion within TC39 and consultation with the developer community. Unfortunately, the community was rather split, while TC39 came down rather strongly on the side of Object.defineProperty
.
As a mitigation, the decorators proposal provides the tools to write a decorator to make a public field declaration use [[Set]]
semantics. Even if you disagree with the default, the other option is available. (This would be the case regardless of which default TC39 chose.)
Public fields are shipping in Chrome 72 with [[Define]]
semantics, and this decision on semantics is unlikely to be revisited.
Both public and private field declarations create a field in the instance, whether or not there's an initializer present. If there's no initializer, the field is set to undefined
. This differs a bit from certain transpiler implementations, which would just entirely ignore a field declaration which has no initializer.
For example, in the following example, new D
would result in an object whose y
property is undefined
, not 1
.
class C {
y = 1;
}
class D extends C {
y;
}
The semantics of setting fields without initializers to undefined
as opposed to erasing them is that field declarations give a reliable basis to ensure that properties are present on objects that are created. This helps programmers keep objects in the same general state, which can make it easy to reason about and, sometimes, more optimizable in implementations.
Private fields are based on syntax using a #
, both when declaring a field and when accessing it.
class X {
#foo;
method() {
console.log(this.#foo)
}
}
This syntax tries to be both terse and intuitive, although it's rather different from other programming languages. See the private syntax FAQ for discussion of alternatives considered and the constraints that led to this syntax.
There are no private computed property names: #foo
is a private identifier, and #[foo]
is a syntax error.
Private fields provide a strong encapsulation boundary: It's impossible to access the private field from outside of the class, unless there is some explicit code to expose it (for example, providing a getter). This differs from JavaScript properties, which support various kinds of reflection and metaprogramming, and is instead analogous to mechanisms like closures and WeakMap
, which don't provide access to their internals. See these FAQ entries for more information on the motivation for this decision.
Some mitigations which make it easier to access
- Implementations' developer tools may provide access to private fields (V8 issue).
- The decorators proposal gives tools for easy-to-use and controlled access to private fields.
Public and private fields are each added to the instance in the order of their declarations, while the constructor is running. The initializer is newly evaluated for each class instance. Fields are added to the instance right after the initializer runs, and before evaluating the following initializer.
Scope: The instance under construction is in scope as the this
value inside the initializer expression. new.target
is undefined, as in methods. References to arguments
are an early error. Super method calls super.method()
are available within initializers, but super constructor calls super()
are a syntax error. await
and yield
are unavailable in initializers, even if the class is declared inside an async function/generator.
When field initializers are evaluated and fields are added to instances:
- Base class: At the beginning of the constructor execution, even before parameter destructuring.
- Derived class: Right after
super()
returns. (The flexibility in howsuper()
can be called has led many implementations to make a separate invisibleinitialize()
method for this case.)
If super()
is not called in a derived class, and instead some other public and private fields are not added to the instance, and initializers are not evaluated. For base classes, initializers are always evaluated, even if the constructor ends up returning something else. The new.initialize
proposal would add a way to programmatically add fields to an instance which doesn't come from super()
/the this
value in the base class.
See the draft specification for full details.
This proposal reached Stage 3 in July 2017. Since that time, there has been extensive thought and lengthy discussion about various alternatives, including:
- JS Classes 1.1
- Reconsideration of "static private"
- Additional use of the
private
keyword - Private Symbols
In considering each proposal, TC39 delegates looked deeply into the motivation, JS developer feedback, and the implications on the future of the language design. In the end, this thought process and continued community engagement led to renewed consensus on the proposal in this repository. Based on that consensus, implementations are moving forward on this proposal.
This document proposes a combined vision for public fields and private fields, drawing on the earlier Orthogonal Classes and Class Evaluation Order proposals. It is written to be forward-compatible with the introduction of private methods and decorators, whose integration is explained in the unified class features proposal. Methods and accessors are defined in a follow-on proposal.
This proposal has been developed in this GitHub repository as well as in presentations and discussions in TC39 meetings. See the past presentations and discussion notes below.
Date | Slides | Notes |
---|---|---|
July 2016 | Private State | π |
January 2017 | Public and private class fields: Where we are and next steps | π |
May 2017 | Class Fields Integrated Proposal | π |
July 2017 | Unified Class Features: A vision of orthogonality | π |
September 2017 | Class fields status update | π |
November 2017 | Class fields, static and private | π |
November 2017 | Class features proposals: Instance features to stage 3 | π |
November 2017 | ASI in class field declarations | π |
May 2018 | Class fields: Stage 3 status update | π |
September 2018 | Class fields and private methods: Stage 3 update | π |
January 2019 | Private fields and methods refresher | π |
You can experiment with the class fields proposal using the following complete implementations:
- Babel 7.0+
- Node 12
- Public fields are enabled by default in Chrome 72 / V8 7.2
- Private fields are enabled by default in Chrome 74 / V8 7.4
- Public instance fields are enabled by default in Firefox 69
- Public static fields are enabled in Firefox Nightly 75 as of Febraury 22, 2020
- Public instance fields are shipped behind a flag in Safari Technology Preview 101
- Moddable XS
- QuickJS
- TypeScript 3.8
Further implementations are on the way:
- Private instance fields, private instance methods, private instance accessors and static fields in Webkit
- Private instance fields and private instance methods and accessors tracking issues in Firefox/SpiderMonkey Bugzilla
- Additional tooling support
You are encouraged to file issues and PRs this repository to
- Ask questions about the proposal, how the syntax works, what the semantics mean, etc.
- Discuss implementation and testing experience, and issues that arise out of that process.
- Develop improved documentation, sample code, and other ways to introduce programmers at all levels to this feature.
If you have any additional ideas on how to improve JavaScript, see ecma262's CONTRIBUTING.md for how to get involved.