ES 提案:私有类字段
类字段是关于直接在类内部创建属性字段和相似的构造。这篇文章是一个系列的一部分:
在这篇文章中,我们将了解私有字段(private fields),一种在实例和类中新种类的私有插槽。这个新功能是由 Daniel Ehrenberg 和 Jeff Morrison 提出的 ES 提案”JavaScript 的类字段声明”的一部分。
概述:
私有字段是一种新的不同于属性的数据插槽。它们只能直接地在声明它们的类内部访问。
私有静态字段
class MyClass {
// Declare and initialize
static #privateStaticField = 1
static getPrivateStaticField() {
return MyClass.#privateStaticField // (A)
}
}
assert.throws(() => eval('MyClass.#privateStaticField'), {
name: 'SyntaxError',
message:
'Undefined private field undefined:'
+ ' must be declared in an enclosing class',
})
assert.equal(MyClass.getPrivateStaticField(), 1)
注意:不要直接通过 this
访问私有静态字段,一直直接使用类名访问(像行 A 那样)。在文章的后面解释为什么这样。
私有实例字段
使用有初始化值的私有字段:
class MyClass {
// Declare and initialize
#privateInstanceField = 2
getPrivateInstanceField() {
return this.#privateInstanceField
}
}
assert.throws(() => eval('new MyClass().#privateInstanceField'), {
name: 'SyntaxError',
message:
'Undefined private field undefined:'
+ ' must be declared in an enclosing class',
})
assert.equal(new MyClass().getPrivateInstanceField(), 2)
使用没有初始化值的私有实例字段:
class DataStore {
#data // must be declared
constructor(data) {
this.#data = data
}
getData() {
return this.#data
}
}
assert.deepEqual(Reflect.ownKeys(new DataStore()), [])
从下划线到私有实例字段
在 JavaScript 中使用私有数据一个常见的方法是在属性名前面添加前置下划线。在本节中,我们开始使用这一方法编码,然后慢慢改变它,直到使用私有实例字段。
使用下划线
class Countdown {
constructor(counter, action) {
this._counter = counter // private
this._action = action // private
}
dec() {
if (this._counter < 1)
return
this._counter--
if (this._counter === 0) {
this._action()
}
}
}
// The data is not really private:
assert.deepEqual(Reflect.ownKeys(new Countdown(5, () => {})), [
'_counter',
'_action',
])
这个方法没有给我们任何的保护;它只是建议使用这个类的人:不要直接使用这些属性,它们被认为是私有的。
这个方法的好处是方便的。通过私有字段,我们不会丧失方便性且收获真正的隐藏。
转换到私有实例字段
我们可以通过两步从下划线方法转换到私有字段:
- 使用哈希符号替换每一个下划线。
- 在类开始的地方声明所有的私有字段。
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()
}
}
}
// The data is now private:
assert.deepEqual(Reflect.ownKeys(new Countdown(5, () => {})), [])
类的主体内的所有代码都可以访问所有私有字段
举例来说,实例方法可以访问私有静态字段:
class MyClass {
static #privateStaticField = 1
getPrivateFieldOfClass(theClass) {
return theClass.#privateStaticField
}
}
assert.equal(new MyClass().getPrivateFieldOfClass(MyClass), 1)
而且静态方法可以访问私有实例字段:
class MyClass {
#privateInstanceField = 2
static getPrivateFieldOfInstance(theInstance) {
return theInstance.#privateInstanceField
}
}
assert.equal(MyClass.getPrivateFieldOfInstance(new MyClass()), 2)
如何管理私有字段
在 ECMAScript 规范中,私有字段通过附加到对象的数据结构进行管理。大概手段如下:
{
// Private names
const _counter = {
__Description__: 'counter',
__Kind__: 'field',
}
const _action = {
__Description__: 'action',
__Kind__: 'field',
}
class Object {
// Maps private names to values
__PrivateFieldValues__ = new Map()
}
class Countdown {
constructor(counter, action) {
this.__PrivateFieldValues__.set(_counter, counter)
this.__PrivateFieldValues__.set(_action, action)
}
dec() {
if (this.__PrivateFieldValues__.get(_counter) < 1)
return
this.__PrivateFieldValues__.set(
_counter,
this.__PrivateFieldValues__.get(_counter) - 1
)
if (this.__PrivateFieldValues__.get(_counter) === 0) {
this.__PrivateFieldValues__.get(_action)()
}
}
}
}
这里有两个重要的点:
- 私有名称(private names)是唯一键。它们只能在类的主体内部访问到。
- 私有字段值(private field values)是私有名称的映射值。每一个实例的私有字段都有对应的值。你只能通过私有键访问到私有字段值。
由此可知:
- 你在类
Countdown
的主体内部只能访问到存储在.#counter
和.#action
,因为你只能通过私有名称。 - 私有字段不能在子类中访问。
隐患:使用 this
访问私有静态字段
你可以使用 this
访问公有静态字段,但是不应该使用它访问私有静态字段。
this
与公有静态字段
思考下面的代码:
class SuperClass {
static publicData = 1
static getPublicViaThis() {
return this.publicData
}
}
class SubClass extends SuperClass {}
公有静态字段是属性,如果我们调用方法:
assert.equal(SuperClass.getPublicViaThis(), 1)
然后 this
指向 SuperClass
且一个按照预期工作。我们可以通过子类引用 .getPublicViaThis()
:
assert.equal(SubClass.getPublicViaThis(), 1)
SubClass
继承了 .getPublicViaThis()
,this
指向 SubClass
后继续运行,因为 SubClass
也继承了属性 .publicData
。
(顺便一提,.publicData
在这种情况下会在内部创建一个 SubClass
非破坏性覆盖 SuperClass
属性的新属性。)
this
与私有静态属性
思考下面的代码:
class SuperClass {
static #privateData = 2
static getPrivateViaThis() {
return this.#privateData
}
static getPrivateViaClassName() {
return SuperClass.#privateData
}
}
class SubClass extends SuperClass {}
因为 this
指向 SuperClass
,通过 SuperClass
引用 .getPrivateViaThis()
可以工作:
assert.equal(SuperClass.getPrivateViaThis(), 2)
然而,通过 SubClass
引用 .getPrivateViaThis()
不能工作,因为现在指向 SubClass
和 SubClass
的 this
没有包含私有静态字段 .#privateData
:
assert.throws(() => SubClass.getPrivateViaThis(), {
name: 'TypeError',
message:
'Read of private field #privateData from'
+ ' an object which did not contain the field',
})
解决方法是直接通过 SuperClass
访问 .#privateData
:
SubClass.getPrivateViaClassName()
朋友(“Friend”) 与 保护(“protected”) 隐私
有时,我们想要实体成为类的“朋友”。如此就能够访问到类的私有数据。在下面的代码,函数 getCounter()
是 Countdown
的朋友。我们使用 WeakMap
使数据私有化,那将允许 getCounter
访问 Countdown
的数据。
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)()
}
}
}
function getCounter(countdown) {
return counter.get(countdown)
}
现在很容易控制谁可以访问私有数据:如果访问了 _counter
和 _action
,也就访问了私有数据。如果我们将上面的代码片段放在一个模块内,整个模块的数据都将是私有的。
更多关于这个技术的信息,请查看 使用 WeakMap 保存私有数据。它包含在父类与子类之间分享私有数据。
常见问题
为什么使用 #
而不是通过 private
声明私有字段?
原则上,私有字段可以像下面这样:
class MyClass {
private value
compare(other) {
return this.value === other.value
}
}
但是然后我们不能在类的主体内部的任何地方使用属性名和值—它总是被解释为私有名称。动态类型语言无法在运行时区分属性是私有的还是公有的。
像 TypeScript 的静态类型语言拥有更好的灵活性:它们在编译阶段知晓 other
是类 MyClass
的实例,然后能够处理 .value
为私有或者不是。
即静态类型语言在编译阶段可以判断字段是公有的还是私有的。
延伸阅读
- JavaScript 中的 “Private data for classes”
- JavaScript 中的 “WeakMaps (WeakMap)”
文章由 吳文俊 翻译,原文地址 ECMAScript proposal: private class fields,转载请注明来源。