/proposal-class-fields

Orthogonally-informed combination of public and private fields proposals

Primary LanguageHTML

Class field declarations for JavaScript

Daniel Ehrenberg, Jeff Morrison

Stage 3

A guiding example: Custom elements with classes

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

Field declarations

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.

Private fields

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.

Major design points

Public fields created with Object.defineProperty

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.

Fields without initializers are set to undefined

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 x property is undefined, not 1.

class C {
  y = 1;
}
class D {
  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 syntax

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.

No backdoor to access private

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.

Specification

See the draft specification for full details.

Status

Consensus in TC39

This proposal reached Stage 3 in July 2017. Since that time, there has been extensive thought and lengthy discussion about various alternatives, including:

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.

Development history

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 📝

Implementations

You can experiment with the class fields proposal using the following complete implementations:

  • Babel 7.0+
  • Public fields are enabled by default in Chrome Canary / V8, private fields are behind a flag in Chrome / V8

Further implementations are on the way:

Activity welcome in this repository

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.
  • Propose and discuss small syntactic or semantic tweaks, especially those motivated by experience implementing or using the proposal.
  • 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.