ECMAScript Private Fields
This proposal extends ECMAScript class syntax by introducing the following features
necessary for supporting high-integrity classes:
- Private Fields allow per-instance state which is inaccessible outside of the class
body.
- Nested Declarations allow function, class, and variable declarations within the
class body which have access to private fields.
A Brief Introduction
Private fields are represented as an identifer prefixed with the @
character. Private
fields are lexically confined to their containing class and are not reified. In the
following example, @x
and @y
are private fields whose type is guaranteed to be
Number.
class Point {
@x;
@y;
constructor(x = 0, y = 0) {
this.@x = +x;
this.@y = +y;
}
get x() { return this.@x }
set x(value) { this.@x = +value }
get y() { return this.@y }
set y(value) { this.@y = +value }
toString() { return `Point<${ this.@x },${ this.@y }>` }
}
An "at-name" can also appear as a primary expression, in which case this
is used
as the implied base value.
class Point {
@x;
@y;
constructor(x = 0, y = 0) {
@x = +x;
@y = +y;
}
get x() { return @x }
set x(value) { @x = +value }
get y() { return @y }
set y(value) { @y = +value }
toString() { return `Point<${ @x },${ @y }>` }
}
In the above class, input values are converted to the Number type. If we wanted
to throw an error when an invalid type is provided, we could use a nested function
declared within the class body.
class Point {
@x;
@y;
constructor(x = 0, y = 0) {
@x = _number(x);
@y = _number(y);
}
get x() { return @x }
set x(value) { @x = _number(value) }
get y() { return @y }
set y(value) { @y = _number(value) }
toString() { return `Point<${ @x },${ @y }>` }
function _number(n) {
if (+n !== n)
throw new TypeError("Not a number");
return n;
}
}
Because private fields are lexically scoped, declarations nested within the class body
can access private state. (This example uses the proposed
function bind operator.)
class Container {
@count = 0;
clear() {
if (this::_isEmpty())
return;
}
function _isEmpty() {
return @count === 0;
}
}
As shown in the previous example, private fields may have an initializer. Private field
initializers are evaluated when the constructor's this value is initialized.
For more complete examples, see:
Syntax
AtName ::
@ IdentifierName
PrivateDeclaration[Yield] :
AtName Initializer[?Yield](opt) ;
ClassElement[Yield] :
PrivateDeclaration[?Yield]
Declaration[?Yield]
VariableStatement[?Yield]
MethodDefinition[?Yield]
static MethodDefinition[?Yield]
;
MemberExpression[Yield] :
...
MemberExpression[?Yield] . AtName
CallExpression[Yield] :
...
CallExpression[?Yield] . AtName
PrimaryExpression[Yield] :
...
AtName
High-Level Semantics
Private Declarations
- A PrivateDeclaration creates a new PrivateMap object bound to the lexical
environment of the containing ClassBody.
- A PrivateMap is a specification type with the following methods:
- has(obj)
- get(obj)
- set(obj, value)
- The semantics of each PrivateMap method is identical to the corresponding method of
the built-in WeakMap type.
Initialization Model
- Each class constructor that contains private fields has an internal slot named
[[PrivateFields]] whose value is a List of PrivateFieldRecord objects
identifying those private fields.
- A PrivateFieldRecord object has the following internal slots:
- [[Map]]: A PrivateMap object.
- [[Initializer]]: The root node of the parse tree of the private field's initializer
expression.
- An empty initializer expression is equivalent to undefined.
- Immediately after initializing the this value associated with a Function
Environment Record to a value V, if the environment's [[FunctionObject]] has a
[[PrivateFields]] list, then:
- Let initialList be an empty List.
- For each field in [[PrivateFields]]:
- If field.[[Initializer]] is empty, then let initialValue be undefined.
- Else
- Let initialValue be the result of evaluating field.[[Initializer]].
- ReturnIfAbrupt(initialValue).
- NOTE: If any initializer throws an exception then no private fields in
[[PrivateFields]] are initialized.
- Append the Record {[[map]]: field.[[Map]], [[value]]: initialValue} to
initialList.
- For each Record e in initialList:
- Perform e.[[map]].set(V, e.[[value]]).
- Initializers are evaluated in a new lexical environment whose this value is
undefined and whose parent lexical environment is identified by the class body.
Private References
- When an AtName is used as a primary expression such as
@field
, it is equivalent to
the member expression this.@field
.
- AtName member expressions return a private reference, whose property name component
is a PrivateMap object.
- When GetValue is called on a private reference V:
- Let privateMap be GetReferencedName(V).
- If privateMap.has(baseValue) is false then throw a TypeError exception.
- Return privateMap.get(baseValue).
- NOTE: The prototype chain is not traversed.
- When SetValue is called on a private reference V with value W:
- Let privateMap be GetReferencedName(V).
- If privateMap.has(baseValue) is false then throw a TypeError exception.
- Return privateMap.set(baseValue, W).
- NOTE: The prototype chain is not traversed.
- GetValue and SetValue, when evaluated for private references, do not tunnel through
proxies.
- Proxies do not trap private field access.