This blog post is part of a series on new members in bodies of class definitions:
In this post, we look at public fields, which create instance properties and static properties.
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.
const staticFieldKey = Symbol('staticFieldKey');
class MyClass {
static staticField = 1;
static [staticFieldKey] = 2; // computed key
}
assert.equal(MyClass.staticField, 1);
assert.equal(MyClass[staticFieldKey], 2);
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);
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);
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.
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;
}
}
The remaining sections cover advanced aspects of public fields.
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?
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'
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.
There are arguments against using definition for public fields:
=
for properties always triggered assignment.These are the arguments in favor of using definition:
As so often, the decision to use definition (and not assignment) is a compromise where the pros and cons were weighed.
The execution of public instance fields roughly follows these two rules:
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'
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);
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”.
Class fields are currently implemented in:
@babel/plugin-proposal-class-properties
.For more information, see the proposal.