That's not quite the same with WeakMap because the access (with WeakMap) can happen outside the class body, whereas the #
syntax cannot be used outside of the class.
Besides that, the indexed access feels nicer, more readable, and class field syntax is more concise.
Compare this,
const prop = new WeakMap()
class MyClass {
constructor() {
prop.set(this, 123)
}
method() {
console.log(prop.get(this))
}
}
const o = new MyClass()
o.method() // logs "123"
console.log(prop.get(o)) // logs "123"
vs this:
const prop = Symbol()
class MyClass {
#[prop] = 123
method() {
console.log(this#[prop])
}
}
const o = new MyClass()
o.method() // logs "123"
console.log(o#[prop]) // SyntaxError
The latter allows "protected" to be possible, while still disallowing public usage.
Protected access can only happen with extends
, and the access can only happen inside the class definition:
export const prop = Symbol()
export class Base {
#[prop] = 123
}
import {prop, Base} from './Base.js'
export class Sub extends Base {
method() {
console.log(this#[prop])
}
}
import {prop} from './Base.js'
import {Sub} from './Sub.js'
const o = new Sub()
o.method() // logs "123"
o#[prop] // SyntaxError
// Access by extension (i.e. protected) is fine:
class MyClass extends Sub {
changeValue() {
this#[prop] = 456
}
}
const o2 = new MyClass()
o2.changeValue()
o2.method() // logs "456"
o2#[prop] // SyntaxError
This would be especially useful for real world use cases like Custom Elements with attachInternals
that people in the wb dev community want to share to subclasses using a protected-like feature. Some info on the problem:
Basically, currently a base custom element class can do this:
export class BaseElement extends HTMLElement {
#internals
constructor() {
super()
this.#internals = this.attachInternals()
}
}
then if the user of the base class wants the internals too, they are out of luck:
import {BaseElement} from 'some-lib'
class SomeElement extends BaseElement {
#int
constructor() {
super()
this.#int = this.attachInternals() // runtime error! attachInternals was already called!
}
method() {
this.#int // use internals
}
}
customElements.define('some-el', MyEl) // "finalize" the class (subclasses of SomeElement will need a new name).
The idea is that an end user of some-el
should not be able to access the internals. For example, the user cannot do this:
const el = document.querySelector('some-el')
el.#int // SyntaxError (good!)
However, the above example does not work in the first place, because the constructor
in the subclass throws. attachInternals
can only be called once (as per the current spec at least for now).
The BaseClass would need to find a way to share the internals with subclasses, and its gets hairy.
With the private class field symbol idea, sharing is easy to do without exposing the internals to public users:
const internals = Symbol()
export class BaseElement extends HTMLElement {
#[internals]
constructor() {
super()
this#[internals] = this.attachInternals()
}
}
import {internals, BaseElement} from 'some-lib'
class SomeElement extends BaseElement {
method() {
this#[internals] // use internals
}
}
customElements.define('some-el', MyEl) // "finalize" the class (subclasses of SomeElement will need a new name).
With this approach, subclasses can easily use the internals, while public users of the element are still restricted:
import {internals} from 'some-lib'
const el = document.querySelector('some-el')
el.method() // ok
el#[internals] // SyntaxError (good!)
With WeakMap
, then the following is possible (and not the desired result):
import {internals} from 'some-lib'
const el = document.querySelector('some-el')
el.method() // ok
internals.get(el) // protected internals are leaked (undesirable)
Once the customElements.define('some-el', SomeElement)
call happens, all some-el
instances in the DOM will be SomeElement
and a user cannot access their internals by class extension. The classes are effectively "finalized".