Private fields and methods can only be accessed from lexically inside the class where they are defined. In many cases, it is important for code more generally to be able to access a private field or method. It's possible to close over access to private fields and methods to grant some other code the capability to access them, but the code to do this is awkward and redundant.
The private.name
syntax creates a reified reference to accessing a private field or method without the need for repetitive arrow functions. As an example of usage:
class C {
#x;
static x = private.name #x;
}
c = new C;
C.x.has(c); // true
C.x.get(c); // undefined
C.x.set(c, 1);
C.x.get(c); // 1
C.x.has({}); // false
C.x.get({}); // TypeError
C.x.set({}, 1); // TypeError
PrivateName
is a constructor, but it throws when called or constructed. It has the following methods:
get(obj)
gets the private field or method or getter on the object, or throws a TypeError if missingset(obj, value)
sets the private field or calls a setter on the object, or throws a TypeError if missing (or on a method)has(obj)
returnstrue
if the object has the private field or method, andfalse
otherwise.
All methods throw a TypeError if passed an argument which is not an object.
The method names here are designed to look "backwards" but make sense directly for ordinary JavaScript programming, and correspond to the WeakMap analogy for private fields when thinking about semantics more deeply.
Private fields and methods are always added to instances in exactly one place: in the constructor, either at the beginning (for a base class) or when super()
returns (for a subclass). Adding a capability for private name references to add a private field or method to an instance in a different way weakens this guarantee, risking violating expectations for class users. The risk is especially serious given how the PrivateName can be shared outside of the class.
Justin Ridgewell argued that, as private fields and methods have syntax that gives it integrity, then metaprogramming over them should also have integrity. To Justin, this would imply that the class's prototype would be frozen, to prevent code from modifying it. This constraint differs from TC39's usual analysis, which assumes that early-running code can be responsible for any freezing necessary; the logic here is that PrivateName represents an especially important case for integrity in practice, even when early-running code may be impractical. This is a point I don't have a strong opinion on; I could see the argument either way.
This gist uses the name private.name
for the keyword, following the keyword.metaproperty
variable scheme of ES6, e.g., new.target
. This syntax avoids syntax issues like the need to ban newlines after contextual keywords. It also evocatively describes what the construct does.
Referring to a private name and exposing it to someone else is a bit of a "big deal"/"powertool", moreso than reading or writing a private field, so the name does not seem excessively/inappropriately long to me.
Some alternatives considered and not selected:
ref #foo
:ref
is a bit of a cryptic name, and might lead to expectations that it could reference other things (like lexically scoped variables).&#foo
: This comes to mind for a lot of people, but it's just too much punctuation!#foo
: A bare private name would be a bit strange, since it would feel like you'd get something that represents the whole name. However, (1) this construct is less powerful than the private name (2) it creates a new instance each time it executes.
#name in obj
: terser, simpler syntax for(private.name #name).has(obj)
which avoids allocating a new PrivateName- Decorators on private fields and methods: Decorated private fields and methods receive a PrivateName instance in place of the property key (early draft)
private.name #foo
would desugar into something like
makePrivateName(x => x.#foo, (x, v) => x.#foo = v)
which could be defined as follows:
class PrivateName {
#get;
#set;
get(obj) {
return this.#get(obj);
}
set(obj, val) {
return this.#set(obj, val);
}
has(obj) {
try {
this.#get(obj);
return true;
} catch {
return false;
}
}
static #allow = false; // Prevent calling the constructor
static make(get, set) {
PrivateName.#allow = true;
const name = new PrivateName(get, set);
PrivateName.#allow = false;
return name;
}
constructor(get, set) {
if (!PrivateName.#allow) throw new TypeError;
this.#get = get;
this.#set = set;
}
}
const makePrivateName = PrivateName.make;
delete PrivateName.make;
Object.freeze(PrivateName);
Object.freeze(PrivateName.prototype);