ECMAScript 提案:JavaScript 类中私有静态方法及其访问器
这篇博客文章是关于类定义主体中新成员的系列文章的一部分:
文章介绍类的私有静态方法及其访问器,由 Shu-yu Guo 和 Daniel Ehrenberg 提出的 ECMAScript 提案 “静态类功能”。
概览:私有静态方法及其访问器
下面是私有静态方法及其访问器存在的类型:
class MyClass {
static #staticPrivateOrdinaryMethod() {}
static *#staticPrivateGeneratorMethod() {}
static async #staticPrivateAsyncMethod() {}
static async *#staticPrivateAsyncGeneratorMethod() {}
static get #staticPrivateGetter() {}
static set #staticPrivateSetter(value) {}
}
私有静态方法的例子
下面的类有一个私有静态方法 .#createInternal()
:
class Point {
static create(x, y) {
return Point.#createInternal(x, y)
}
static createZero() {
return Point.#createInternal(0, 0)
}
static #createInternal(x, y) {
const p = new Point()
p.#x = x // (A)
p.#y = y // (B)
return p
}
#x
#y
}
这段代码显示了私有静态方法的关键好处,与外部(模块-私有)帮助函数相比。他们可以访问私有的实例字段(行 A 和 行 B)。
隐患:不要通过 this
访问私有静态成员
通过 this
访问私有静态成员,可以让我们避免重复使用类名。它之所以有效,是因为这些成员是由子类继承的。但是,我们不能对私有静态构造做同样的事情:
class SuperClass {
static #privateData = 2
static getPrivateDataViaThis() {
return this.#privateData
}
static getPrivateDataViaClassName() {
return SuperClass.#privateData
}
}
class SubClass extends SuperClass {}
// 成功:
assert.equal(SuperClass.getPrivateDataViaThis(), 2)
// 报错:
assert.throws(
() => SubClass.getPrivateDataViaThis(), // (A)
{
name: 'TypeError',
message:
'Cannot read private member #privateData '
+ 'from an object whose class did not declare it',
}
)
// 成功:
assert.equal(SubClass.getPrivateDataViaClassName(), 2)
行 A 的问题是 this
指向没有私有字段 .#privateData
的子类 SubClass
。想要了解更多,请阅读文章 私有类字段。
私有静态字段、方法及其访问器的规范
私人原型方法奠定的基础
对静态方法和访问器的支持是基于为原型方法和访问器引入的机制(更多信息)。
我们将只研究私有方法,但我们发现的一切也适用于私有的获取和设置。
存储私有方法
考虑一个方法 .#privateMethod()
,它是在对象 HomeObj
的“内部”创建的。这个方法被存储在外部,被称为 private name 的规范数据结构中。私有名也用于表示其他私有类元素。它们通过私有环境来查找,私有环境将标识符映射到私有名,并存在于变量的环境中。私有环境将在后面解释。
在本例中,私有名有以下插槽:
.[[Description]] = "#privateMethod"
.[[Kind]] = "method"
.[[Brand]] = HomeObj
.[[Value]]
指向一个方法对象
一个私有方法的令牌是对象内部创建的。
对象的私有令牌
每个对象 Obj
都有一个内部槽 Obj.[[PrivateBrands]]
,它包含了所有可以在 Obj
上调用的方法的令牌。 有两种方式可以将元素添加到对象的私有令牌中:
- 当一个类
C
被 new-invoked 时,它将C.prototype
添加到this
的私有令牌中。这意味着C
的私有原型方法(其令牌为C.prototype
)可以在this
上被调用。 - 如果一个类
C
有私有静态方法,则在C
的私有令牌中加入C.prototype
,也就是说C
的私有静态方法(其令牌是C.prototype
)可以在C
上被调用。
私有令牌与原型链
因此,一个对象的私有令牌与一个对象的原型链有关。既然如此相似,为什么要引入这种机制呢?
- 私有方法的设计实际上是私有的,并且具有完整性。也就是说,它们不应该受到外界变化的影响。如果一个对象的私有令牌是由它的原型链决定的,我们可以通过改变链来启用或禁用私有方法。我们还可以通过
Proxy
观察原型链的遍历,来观察私有方法的部分执行情况。 - 这种方法可以保证,当我们在一个对象上调用一个私有方法时,它的私有字段也是存在的(由构造函数和类定义的评估所创建)。否则,我们可以使用
Object.create()
来创建一个没有私有实例字段的实例,我们可以对其应用私有方法。
私有标识符的语法范围
执行上下文现在有三个环境:
LexicalEnvironment
指向let
和const
的环境(块作用域)。VariableEnvironment
指向var
的环境(函数作用域)。PrivateEnvironment
指向一个环境,它将以#
为前缀的标识符映射到私有名称记录。
函数现在有两个语法环境:
[[Environment]]
指的是创建函数的作用域的环境。[[PrivateEnvironment]]
指的是函数创建时处于活动状态的私有名环境。
操作 ClassDefinitionEvaluation
暂时改变类主体的当前执行环境:
LexicalEnvironment
被设置为classScope
,一个新的声明性环境。PrivateEnvironment
被设置为classPrivateEnvironment
,一个新的声明性环境。
对于类体的 PrivateBoundIdentifiers
中的每一个标识符 dn
,classPrivateEnvironment
的 EnvironmentRecord
中会增加一个条目。该条目的键是 dn
,值是一个新的私有名。
私人静态结构的规范
运行时语义规则 ClassDefinitionEvaluation
的以下部分与静态私有成员(F
指构造函数)有关。
- 章节 28.b.i:对每个静态
ClassElement
执行PropertyDefinitionEvaluation(F, false)
- 章节 28.d.ii:如果结果不是空的,它将被添加到静态字段列表中(以便以后可以附加到
F
中)。
- 章节 28.d.ii:如果结果不是空的,它将被添加到静态字段列表中(以便以后可以附加到
- 章节 33.a:如果
ClassBody
的PrivateBoundIdentifiers
中有一个静态方法或访问器P
,并且P
的.[[Brand]]
是F
:执行PrivateBrandAdd(F, F)
。直观地说,这意味着:对象F
可以成为存储在对象F
中的方法的接收者。 - 章节 34.a:对于静态字段列表中的每个
fieldRecord
:DefineField(F, fieldRecord)
。
在 JavaScript 中说明私有静态方法和私有实例字段的内部表示方法
我们来看一个例子。请看下面这篇文章前面的代码:
class Point {
static create(x, y) {
return Point.#createInternal(x, y)
}
static createZero() {
return Point.#createInternal(0, 0)
}
static #createInternal(x, y) {
const p = new Point()
p.#x = x
p.#y = y
return p
}
#x
#y
toArray() {
return [this.#x, this.#y]
}
}
从内部来看,大致表示如下:
{
// Begin of class scope
class Object {
// Maps private names to values (a list in the spec).
__PrivateFieldValues__ = new Map()
// Prototypes with associated private members
__PrivateBrands__ = []
}
// Private name
const __x = {
__Description__: '#x',
__Kind__: 'field',
}
// Private name
const __y = {
__Description__: '#y',
__Kind__: 'field',
}
class Point extends Object {
static __PrivateBrands__ = [Point]
static __PrivateBrand__ = Point.prototype
static __Fields__ = [__x, __y]
static create(x, y) {
PrivateBrandCheck(Point, __createInternal)
return __createInternal.__Value__.call(Point, x, y)
}
static createZero() {
PrivateBrandCheck(Point, __createInternal)
return __createInternal.__Value__.call(Point, 0, 0)
}
constructor() {
super()
// Setup before constructor
InitializeInstanceElements(this, Point)
// Constructor itself is empty
}
toArray() {
return [
this.__PrivateFieldValues__.get(__x),
this.__PrivateFieldValues__.get(__y),
]
}
dist() {
PrivateBrandCheck(this, __computeDist)
__computeDist.__Value__.call(this)
}
}
// Private name
const __createInternal = {
__Description__: '#createInternal',
__Kind__: 'method',
__Brand__: Point,
__Value__(x, y) {
const p = new Point()
p.__PrivateFieldValues__.set(__x, x)
p.__PrivateFieldValues__.set(__y, y)
return p
},
}
} // End of class scope
function InitializeInstanceElements(O, constructor) {
if (constructor.__PrivateBrand__) {
O.__PrivateBrands__.push(constructor.__PrivateBrand__)
}
const fieldRecords = constructor.__Fields__
for (const fieldRecord of fieldRecords) {
O.__PrivateFieldValues__.set(fieldRecord, undefined)
}
}
function PrivateBrandCheck(obj, privateName) {
if (!obj.__PrivateBrands__.includes(privateName.__Brand__)) {
throw new TypeError()
}
}
本文由 吳文俊 翻译,原文地址 ECMAScript proposal: private static methods and accessors in classes