ES proposal: class fields

[2017-07-28] dev, javascript, esnext, es proposal, classes
(Ad, please don’t block)
Warning: This blog post is outdated. Instead, read section “Public slots (properties) vs. private slots” in “JavaScript for impatient programmers”.

The ECMAScript proposal “Class Fields” by Daniel Ehrenberg and Jeff Morrison is currently at stage 3. This blog post explains how it works.

Overview  

Field declarations:

class MyClass {
    instanceProperty = 0;
    static staticProperty = 0;
}

Private fields have names starting with a # and are only visible within the body of the class:

class MyClass {
    #foo; // must be declared
    constructor(foo) {
        this.#foo = foo;
    }
    incFoo() {
        this.#foo++;
    }
}

Declaring fields  

With this proposal, objects now have two kinds of fields:

  • Properties, whose keys are strings or symbols.
  • Private fields that have names. More on private fields later.

Fields can be configured as follows:

  • Location of property:
    • Static: prefix static
    • Instance: no prefix
  • Visibility and name. A field can be either:
    • A public property with a fixed name
    • A public property with a computed key
    • A private field with a fixed name
  • Initializer: optional
Location Visibility/name
foo; instance public
#foo; instance private
['f'+'oo']; instance computed
static foo; static public
static #foo; static private
static ['f'+'oo']; static computed

Initializers  

With an initializer, you create a property and assign it a value at the same time. In the following code, = 0 is an initializer:

class MyClass {
    x = 0;
    y = 0;
}

This class is equivalent to:

class MyClass {
    constructor() {
        this.x = 0;
        this.y = 0;
    }
}

Initializers are executed before the constructor  

class MyClass {
    foo = console.log('initializer');
    constructor() {
        console.log('constructor');
    }
}
new MyClass();
// Output:
// initializer
// constructor

Location of the field  

Instance fields  

Without any prefix, a declaration creates an instance field:

class MyClass {
    foo = 123;
}
console.log(new MyClass().foo); // 123
console.log(Reflect.ownKeys(new MyClass()));
    // ['foo']

Class fields  

Declarations with the prefix static create fields for classes:

class MyClass {
    static foo = 123;
}
console.log(MyClass.foo); // 123
console.log(Reflect.ownKeys(MyClass)); // ['foo']
    // ['length', 'name', 'prototype', 'foo']

MyClass has the properties length and name, because it is also a function.

Private visibility  

Kinds of privacy in JavaScript  

In ES6 and later, you can already implement two kinds of privacy:

  • Soft privacy (that you can work around): put data into properties whose keys are symbols (details).
    const _counter = Symbol('counter');
    const _action = Symbol('action');
    
    class Countdown {
        constructor(counter, action) {
            this[_counter] = counter;
            this[_action] = action;
        }
        dec() {
            if (this[_counter] < 1) return;
            this[_counter]--;
            if (this[_counter] === 0) {
                this[_action]();
            }
        }
    }    
    
  • Hard privacy (that is mostly safe; for complete safety, you have to take additional precautions): data is in the values of WeakMaps whose keys are the objects that the private data is associated with (details).
    const _counter = new WeakMap();
    const _action = new WeakMap();
    class Countdown {
        constructor(counter, action) {
            _counter.set(this, counter);
            _action.set(this, action);
        }
        dec() {
            let counter = _counter.get(this);
            if (counter < 1) return;
            counter--;
            _counter.set(this, counter);
            if (counter === 0) {
                _action.get(this)();
            }
        }
    }    
    

Private fields are basically a more convenient way of doing hard privacy.

From underscores to private fields  

Another common technique is to indicate which properties are considered private by prefixing their names with underscores:

class Countdown {
    constructor(counter, action) {
        this._counter = counter;
        this._action = action;
    }
    dec() {
        if (this._counter < 1) return;
        this._counter--;
        if (this._counter === 0) {
            this._action();
        }
    }
}

This technique doesn’t give you any protection, but it is more convenient than using symbols or WeakMaps.

Such code can be changed to use the new private field feature in two steps:

  1. Replace each underscore with a hash symbol.
  2. Declare all private fields at the beginning of the class.
class Countdown {
    #counter;
    #action;

    constructor(counter, action) {
        this.#counter = counter;
        this.#action = action;
    }
    dec() {
        if (this.#counter < 1) return;
        this.#counter--;
        if (this.#counter === 0) {
            this.#action();
        }
    }
}

Countdown does not have any instance properties:

const countdown = new Countdown(5, () => {});
Reflect.ownKeys(countdown); // []

Private fields are similar to hard privacy via WeakMaps  

In the spec, private fields are managed via a data structure that is attached to objects. That is, private fields are roughly handled as follows.

{
    const _counter = Symbol();
    const _action = Symbol();

    class Countdown {

        __PrivateFieldValues__ = {
            [_counter]: undefined,
            [_action]: undefined,
        };

        constructor(counter, action) {
            this.__PrivateFieldValues__[_counter] = counter;
            this.__PrivateFieldValues__[_action] = action;
        }

        ···
    }    
}

A consequence of this approach is that you can only access private fields if you are inside the body of a class; access to this does not give you access to private data. In other words, you need to know the right symbol to access the data (__PrivateFieldValues__ in the example is not fully protected, but the corresponding data structure in the spec is).

More information in the spec: Sect. “Private Names and references

Not yet supported  

Two elements of classes cannot yet be private:

  • Method definitions
  • Setters and getters

An upcoming proposal that fills this gap is currently at stage 2.

Trying out this proposal  

If you switch on stages 2+ in the Babel REPL, you can play with class fields (however, private fields are not supported, yet).

FAQ  

Why the #? Why not declare private fields via private?  

First, the # clearly indicates that private fields are not properties. They are a completely different mechanism.

Second, if you declared private fields via private and if they looked like normal properties, property lookup would become more complicated:

class MyClass {
    private name;
    compare(other) {
        return this.name === other.name;
    }
}

If other is a direct(!) instance of MyClass, other.name is a private field. Otherwise, it refers to a normal property.

Further reading