ES 提案:私有类字段

ES 提案:私有类字段

类字段是关于直接在类内部创建属性字段和相似的构造。这篇文章是一个系列的一部分:

  1. 公有类字段
  2. 私有类字段
  3. JavaScript 类中的私有方法和其读写
  4. JavaScript 类中的私有静态方法和其读写

在这篇文章中,我们将了解私有字段(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',
]);

这个方法没有给我们任何的保护;它只是建议使用这个类的人:不要直接使用这些属性,它们被认为是私有的。

这个方法的好处是方便的。通过私有字段,我们不会丧失方便性且收获真正的隐藏。

转换到私有实例字段

我们可以通过两步从下划线方法转换到私有字段:

  1. 使用哈希符号替换每一个下划线。
  2. 在类开始的地方声明所有的私有字段。
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() 不能工作,因为现在指向 SubClassSubClassthis 没有包含私有静态字段 .#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);
}

现在很容易控制谁可以访问私有数据:如果访问了 counteraction,也就访问了私有数据。如果我们将上面的代码片段放在一个模块内,整个模块的数据都将是私有的。

更多关于这个技术的信息,请查看 使用 WeakMap 保存私有数据。它包含在父类与子类之间分享私有数据。

常见问题

为什么使用 # 而不是通过 private 声明私有字段?

原则上,私有字段可以像下面这样:

class MyClass {
  private value;
  compare(other) {
    return this.value === other.value;
  }
}

但是然后我们不能在类的主体内部的任何地方使用属性名和值--它总是被解释为私有名称。动态类型语言无法在运行时区分属性是私有的还是公有的。

像 TypeScript 的静态类型语言拥有更好的灵活性:它们在编译阶段知晓 other 是类 MyClass 的实例,然后能够处理 .value 为私有或者不是。

即静态类型语言在编译阶段可以判断字段是公有的还是私有的。

延伸阅读

本文作者 Axel Rauschmayer,转载请注明来源链接:

原文链接:https://2ality.com/2019/07/private-class-fields.html

本文链接:https://tie.pub/2019/07/private-class-fields/