原文链接:https://2ality.com/2019/07/private-class-fields.html
class属性是关于直接在类体内创建属性和类似构造,这篇播客文章是关于它们的系列文章的一部分:
- 公有class属性
- 私有class属性
在这篇文章中,我们看看私有属性,在类和实例中的一种新的私有插槽。这个功能是ES草案“JavaScript类属性声明”的一部分。
Private fields是一种与属性不同的新的数据插槽。它们只能在它们声明的class body内直接访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
class MyClass {
// 声明并初始化静态私有属性
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行那样)。为什么会在本文后面解释。
使用带有初始值的私有属性(等号后面跟值)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
class MyClass {
// 声明并初始化
#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);
|
使用未初始化的实例私有属性:
1
2
3
4
5
6
7
8
9
10
11
12
|
class DataStore {
#data; // 必须声明
constructor(data) {
this.#data = data;
}
getData() {
return this.#data;
}
}
assert.deepEqual(
Reflect.ownKeys(new DataStore()),
[]);
|
在JavaScript中保持数据私有的公共约定是使用带有下划线的属性名。在这个部分,我们从使用这个约定的代码开始,然后更改它,到使用实例私有属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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();
}
}
}
// 变量并非真正的私有
assert.deepEqual(
Reflect.ownKeys(new Countdown(5, () => {})),
['_counter', '_action']);
|
这个约定并没有给我们任何保护;它仅仅建议使用这个类的用户:不要使用这些属性,它们是私有的。
我们可以用实例私有属性代替下划线:
- 我们用哈希符
#
替换每一个下划线_
。
- 我们在类的顶部声明所有的私有属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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, () => {})),
[]);
|
例如,实例方法可以访问静态私有属性:
1
2
3
4
5
6
7
8
|
class MyClass {
static #privateStaticField = 1;
getPrivateFieldOfClass(theClass) {
return theClass.#privateStaticField;
}
}
assert.equal(
new MyClass().getPrivateFieldOfClass(MyClass), 1);
|
一个静态方法可以访问实例私有属性:
1
2
3
4
5
6
7
8
|
class MyClass {
#privateInstanceField = 2;
static getPrivateFieldOfInstance(theInstance) {
return theInstance.#privateInstanceField;
}
}
assert.equal(
MyClass.getPrivateFieldOfInstance(new MyClass()), 2);
|
剩余部分是关于类的私有属性的高级用法
在规范中,私有属性通过附加到对象上的数据结构进行管理。私有属性的大致处理过程如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
{
// 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)();
}
}
}
}
|
这里有两点很重要:
- 私有名称是唯一的key。它们只能在class内访问。
- 私有属性的值是一个私有名称(key)=>值(value)的字典。每个拥有私有属性的实例都有这样一个字典。只能通过私有变量的key访问其value。
意义:
- 在类
Countdown
中,只能访问存储在.#counter
和 #action
的私有数据——因为在这个类中你只有这两个私有变量。
- 私有属性不能被子类访问。
你可以使用this
访问公有静态属性,但是你不应该使用它访问私有静态属性。
1
2
3
4
5
6
7
8
9
|
class SuperClass {
static publicData = 1;
static getPublicViaThis() {
return this.publicData;
}
}
class SubClass extends SuperClass {
}
|
公有静态属性(Public static fields)是属性。如果我们用一个方法调用:
1
|
assert.equal(SuperClass.getPublicViaThis(), 1);
|
this
指向SuperClass
,一切都按照我们预想的那样工作。我们也可以通过子类调用.getPublicViaThis()
。
1
|
assert.equal(SubClass.getPublicViaThis(), 1);
|
SubClass
继承了.getPublicViaThis()
方法,this
指向SubClass
,代码依旧可以运用,因为SubClass
也继承了.publicData
。
(注:在这种情况下设置.publicData
会在SubClass
上创建新的属性,这个属性不会覆盖在SuperClass
上定义的属性)
考虑这段代码:
1
2
3
4
5
6
7
8
9
10
11
|
class SuperClass {
static #privateData = 2;
static getPrivateViaThis() {
return this.#privateData;
}
static getPrivateViaClassName() {
return SuperClass.#privateData;
}
}
class SubClass extends SuperClass {
}
|
通过SuperClass
可以调用.getPrivateViaThis()
,因为this
指向SuperClass
:
1
|
assert.equal(SuperClass.getPrivateViaThis(), 2);
|
然而,通过SubClass
无法调用.getPrivateViaThis()
,因为此时this
指向SubClass
,而SubClass
没有私有静态变量.#privateData
:
1
2
3
4
5
6
7
8
|
assert.throws(
() => SubClass.getPrivateViaThis(),
{
name: 'TypeError',
message: 'Read of private field #privateData from' +
' an object which did not contain the field',
}
);
|
解决办法是直接通过SuperClass
访问.#privateData
,就像SuperClass
中的getPrivateViaClassName
方法。
1
|
assert.equal(SubClass.getPrivateViaClassName(), 2);
|
有时候我们想要某些实体成为某一类的“朋友”。这个朋友应该可以访问calss的私有数据(译者注:这里其实就是想控制哪些可以访问私有属性,哪些不可以)。在以下代码中,函数getCounter()
是类Countdown
的朋友。我们通过使用WeakMaps
生成私有数据,这样Countdown
就允许朋友们访问该数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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
,它们就可以访问私有数据。如果我们将前面的代码片段放到一个模块中,那么数据在整个模块中是私有的。
有关此技术的更多信息,可以查看Sect的“使用WeakMaps
保持私有数据”。这同样适用于在superclass和subclass之间共享私有数据(“被保护”的可见度)。
原则上,私有属性应该用如下方式声明:
1
2
3
4
5
6
|
class MyClass {
private value;
compare(other) {
return this.value === other.value;
}
}
|
但是我们不能在整个class内的任何地方使用属性名值——它们总是被解释为私有变量。
静态类型语言,例如TypeScript在这方面具有更多的灵活性:它们在编译的时候就知道是否是MyClass的一个实例并且可以是否将.value
作为一个私有变量对待。
- class私有属性(private class fields)
- 静态私有属性(private static fields)
- 实例私有属性(private instance fields)