ECMAScript proposal: public class fields

[2019-07-03] dev, javascript, es proposal, js classes
(Ad, please don’t block)

This blog post is part of a series on new members in bodies of class definitions:

  1. Public class fields
  2. Private class fields
  3. Private prototype methods and getter/setters in classes
  4. Private static methods and getter/setters in classes

In this post, we look at public fields, which create instance properties and static properties.

Overview  

Public instance fields  

const instFieldKey = Symbol('instFieldKey');
class MyClass {
  instField = 1;
  [instFieldKey] = 2; // computed key
}
const instance = new MyClass();
assert.equal(instance.instField, 1);
assert.equal(instance[instFieldKey], 2);

Computed field keys are similar to computed property keys in object literals.

Public static fields  

const staticFieldKey = Symbol('staticFieldKey');
class MyClass {
  static staticField = 1;
  static [staticFieldKey] = 2; // computed key
}
assert.equal(MyClass.staticField, 1);
assert.equal(MyClass[staticFieldKey], 2);

Public instance fields and public static fields  

Public instance fields  

The motivation for public fields is as follows.

Sometimes, you have an assignment in a constructor that creates an instance property, but is not influenced by any other data in the constructor (such as a parameter):

class MyClass {
  constructor() {
    this.counter = 0;
  }
}
assert.equal(new MyClass().counter, 0);

In such a case, you can use a field to move the creation of counter out of the constructor:

class MyClass {
  counter = 0;
  constructor() {
  }
}
assert.equal(new MyClass().counter, 0);

You can also omit the initializer (= 0). In that case, the property is initialized with undefined:

class MyClass {
  counter;
  constructor() {
  }
}
assert.equal(new MyClass().counter, undefined);

Public static fields  

At the moment, JavaScript has no way of creating a static property within a class; you have to create it via an external assignment:

class MyClass {
}
MyClass.prop = 123;
assert.equal(MyClass.prop, 123);

One work-around is to create a static getter:

class MyClass {
  static get prop() {
    return 123;
  }
}
assert.equal(MyClass.prop, 123);

A static field provides a more elegant solution:

class MyClass {
  static prop = 123;
}
assert.equal(MyClass.prop, 123);

Why the name public fields?  

Public fields create properties. They have the name “fields” to emphasize how syntactically similar they are to private fields (which are the subject of an upcoming blog post). Private fields do not create properties.

Similarly, “public” describes the nature of public fields, when compared to private fields.

Example: replacing a constructor with a field  

This is a brief, more realistic example where we can also replace the constructor with a field:

class StringBuilder {
  constructor() {
    this.data = '';
  }
  add(str) {
    this.data += str;
    return this;
  }
  toString() {
    return this.data;
  }
}
assert.equal(
  new StringBuilder().add('Hello').add(' world!').toString(),
  'Hello world!');

If we move the creation of .data out of the constructor, we don’t need the constructor, anymore:

class StringBuilder {
  data = '';
  add(str) {
    this.data += str;
    return this;
  }
  toString() {
    return this.data;
  }
}

(Advanced)  

The remaining sections cover advanced aspects of public fields.

Assignment vs. definition  

There is one important way in which creating a property via a constructor and creating a property via a field differ: The former uses assignment; the latter uses definition. What do these two terms mean?

Assigning to properties  

Let’s first take a look at how assigning to properties works with plain objects. This operation is triggered by the assignment operator (=). In the following example, we assign to property .prop (line A):

const proto = {
  set prop(value) {
    console.log('SETTER: '+value);
  }  
}
const obj = {
  __proto__: proto
}
obj.prop = 123; // (A)
assert.equal(obj.prop, undefined);

// Output:
// 'SETTER: 123'

In classes, creating a property via assigning also invokes a setter (if there is one). In the following example, we create property .prop (line A):

class A {
  set prop(value) {
    console.log('SETTER: '+value);
  }
}
class B extends A {
  constructor() {
    super();
    this.prop = 123; // (A)
  }
}
assert.equal(new B().prop, undefined);

// Output:
// 'SETTER: 123'

Defining properties  

Again, we are starting our examination with plain objects. How does defining a property work? There is no operator for defining, we need to use the helper method Object.defineProperty():

const proto = {
  set prop(value) {
    console.log('SETTER: '+value);
  }  
}
const obj = {
  __proto__: proto
}
Object.defineProperty(obj, 'prop', {value: 123});
assert.equal(obj.prop, 123);

The last argument of .defineProperty() is a property descriptor, an object that specifies the attributes (characteristics) of a property. value is one such characteristic. Others include writable – whether the value of a property can be changed.

A public field creates a property via definition, not via assignment:

class A {
  set prop(value) {
    console.log('SETTER: '+value);
  }
}
class B extends A {
  prop = 123;
}
assert.equal(new B().prop, 123);

That is, public fields always create properties and ignore setters.

The pros and cons of using definition for public fields  

There are arguments against using definition for public fields:

  • If we move the creation of a property out of the constructor, to a field, then the behavior of our code changes. That is a refactoring hazard.
  • Until now, using the assignment operator = for properties always triggered assignment.

These are the arguments in favor of using definition:

  • The mental model for entities declared at the top level of a class is overriding: The entity will always be created, independently of what is inherited.
  • Precedents for creating properties via definition include: property definitions in object literals and prototype declarations in classes.

As so often, the decision to use definition (and not assignment) is a compromise where the pros and cons were weighed.

When are public instance fields executed?  

The execution of public instance fields roughly follows these two rules:

  • In base classes, public instance fields are executed immediately before the constructor.
  • In derived classes, public instance fields are executed immediately after super().

This is what that looks like:

class SuperClass {
  superProp = console.log('superProp');
  constructor() {
    console.log('super-constructor');
  }
}
class SubClass extends SuperClass {
  subProp = console.log('subProp');
  constructor() {
    console.log('Before super()');
    super();
    console.log('sub-constructor');
  }
}
new SubClass();

// Output:
// 'Before super()'
// 'superProp'
// 'super-constructor'
// 'subProp'
// 'sub-constructor'

The scope of field initializers  

In the initializer of a public instance field, this refers to the current instance:

class MyClass {
  prop = this;
}
const instance = new MyClass();
assert.equal(instance.prop, instance);

In the initializer of a public static field, this refers to the current class:

class MyClass {
  static prop = this;
}
assert.equal(MyClass.prop, MyClass);

Additionally, super works as expected:

class SuperClass {
  getValue() {
    return 123;
  }
}
class SubClass extends SuperClass {
  prop = super.getValue();
}
assert.equal(new SubClass().prop, 123);

The property attributes of public fields  

By default, public fields are writable, enumerable and configurable:

class MyClass {
  static publicStaticField;
  publicInstanceField;
}
assert.deepEqual(
  Object.getOwnPropertyDescriptor(
    MyClass, 'publicStaticField'),
    {
      value: undefined,
      writable: true,
      enumerable: true,
      configurable: true
    });
assert.deepEqual(
  Object.getOwnPropertyDescriptor(
    new MyClass(), 'publicInstanceField'),
    {
      value: undefined,
      writable: true,
      enumerable: true,
      configurable: true
    });

For more information on the property attributes value, writable, enumerable and configurable, see “JavaScript for impatient programmers”.

Availability  

Class fields are currently implemented in:

For more information, see the proposal.