ECMAScript 提案:JavaScript 类中私有静态方法及其访问器

ECMAScript 提案:JavaScript 类中私有静态方法及其访问器

这篇博客文章是关于类定义主体中新成员的系列文章的一部分:

  1. 公有类字段
  2. 私有类字段
  3. JavaScript 类中的私有方法及其读写
  4. 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 指向 letconst 的环境(块作用域)。
  • VariableEnvironment 指向 var 的环境(函数作用域)。
  • PrivateEnvironment 指向一个环境,它将以 # 为前缀的标识符映射到私有名称记录。

函数现在有两个语法环境:

  • [[Environment]] 指的是创建函数的作用域的环境。
  • [[PrivateEnvironment]] 指的是函数创建时处于活动状态的私有名环境。

操作 ClassDefinitionEvaluation 暂时改变类主体的当前执行环境:

  • LexicalEnvironment 被设置为 classScope,一个新的声明性环境。
  • PrivateEnvironment 被设置为 classPrivateEnvironment,一个新的声明性环境。

对于类体的 PrivateBoundIdentifiers 中的每一个标识符 dnclassPrivateEnvironmentEnvironmentRecord 中会增加一个条目。该条目的键是 dn,值是一个新的私有名。

私人静态结构的规范

运行时语义规则 ClassDefinitionEvaluation 的以下部分与静态私有成员(F 指构造函数)有关。

  • 章节 28.b.i:对每个静态 ClassElement 执行 PropertyDefinitionEvaluation(F, false)
    • 章节 28.d.ii:如果结果不是空的,它将被添加到静态字段列表中(以便以后可以附加到 F 中)。
  • 章节 33.a:如果 ClassBodyPrivateBoundIdentifiers 中有一个静态方法或访问器 P,并且 P.[[Brand]]F:执行 PrivateBrandAdd(F, F)。直观地说,这意味着:对象 F 可以成为存储在对象 F 中的方法的接收者。
  • 章节 34.a:对于静态字段列表中的每个 fieldRecordDefineField(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__: function (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();
  }
}