ES 提案:公有类字段

ES 提案:公有类字段

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

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

在这篇文章中,我们将了解公有字段(public fields),创建的实例属性和静态属性。这个新功能是由 Daniel Ehrenberg 和 Jeff Morrison 提出的 ES 提案"JavaScript 的类字段声明"的一部分。

概述:

公有实例字段

const instFieldKey = Symbol('instFieldKey');
class MyClass {
  instField = 1;
  [instFieldKey] = 2; // computed key(计算字段键)
}
const instance = new MyClass();
assert.equal(instance.instField, 1);
assert.equal(instance[instFieldKey], 2);

计算字段键类似于对象字面量计算属性键

公有静态字段

const staticFieldKey = Symbol('staticFieldKey');
class MyClass {
  static staticField = 1;
  static [staticFieldKey] = 2; // computed key(计算字段键)
}
assert.equal(MyClass.staticField, 1);
assert.equal(MyClass[staticFieldKey], 2);

公有实例字段与公有静态字段

公有实例字段

公有字段的动机如下。

有时,在构造函数中有一个创建实例属性的赋值,但不受构造函数中任何其它数据的影响(例如参数):

class MyClass {
  constructor() {
    this.counter = 0;
  }
}
assert.equal(new MyClass().counter, 0);

在这种情况下,你可以移动 counter 字段到构造函数的外面来创建:

class MyClass {
  counter = 0;
  constructor() {}
}
assert.equal(new MyClass().counter, 0);

可以删除初始化值(= 0)。在相同的情况下,属性使用 undefined 初始化:

class MyClass {
  counter;
  constructor() {}
}
assert.equal(new MyClass().counter, undefined);

公有静态字段

JavaScript 没有办法在类内部创建一个静态属性,你可以通过外部任务创建它:

class MyClass {}
MyClass.prop = 123;
assert.equal(MyClass.prop, 123);

一种办法是创建一个静态 getter

class MyClass {
  static get prop() {
    return 123;
  }
}
assert.equal(MyClass.prop, 123);

静态字段提供一个更为优雅的解决方案:

class MyClass {
  static prop = 123;
}
assert.equal(MyClass.prop, 123);

为什么叫做公有字段(public fields)

公有字段创建属性。它们的名称是“fields”,以强调它们与私有字段(private fields)在语法上的相似性。私有字段不会创建属性。

相似地,“public”描述了公有字段与私有字段相比的本质。

举例:使用字段转换构造函数

这是一个简短且可行的例子来表示我们可以使用字段转换构造函数:

class StringBuilder {
  constructor() {
    this.data = '';
  }
  add(str) {
    this.data += str;
    return this;
  }
  toString() {
    return this.data;
  }
}
assert.equal(
  new StringBuilder().add('Hello').add(' world!').toString(),
  'Hello world!',
);

如果我们移动 .data 到构造函数的外面,我们将不需要构造函数:

class StringBuilder {
  data = '';
  add(str) {
    this.data += str;
    return this;
  }
  toString() {
    return this.data;
  }
}

分配 vs 定义

有一种重要的方法分辨通过构造函数创建属性和通过字段创建属性的不同:前者使用assignment(赋值);后者使用definition(定义)。这两个术语是什么意思?

分配属性

让我们看看如何给一个普通对象分配属性。这项操作通过赋值运算符(=)来完成的。在下面的例子,我们指定属性 .prop(行 A):

const proto = {
  set prop(value) {
    console.log('SETTER: ' + value);
  },
};
const obj = {
  __proto__: proto,
};
obj.prop = 123; // (A)assert.equal(obj.prop, undefined);

// Output:
// 'SETTER: 123'

在类中,创建属性赋值也可以通过引用 setter(如果有的话)。在下面的例子中,我们创建了 .prop 属性(行 A):

class A {
  set prop(value) {
    console.log('SETTER: ' + value);
  }
}
class B extends A {
  constructor() {
    super();
    this.prop = 123; // (A)  }
}
assert.equal(new B().prop, undefined);

// Output:
// 'SETTER: 123'

定义属性

再一次,我们使用普通对象开始。如何定义一个属性?这里没有定义的操作符,我们需要使用方法 Object.defineProperty()

const proto = {
  set prop(value) {
    console.log('SETTER: ' + value);
  },
};
const obj = {
  __proto__: proto,
};
Object.defineProperty(obj, 'prop', { value: 123 });
assert.equal(obj.prop, 123);

.defineProperty() 的最后一个参数是一个属性声明(property descriptor),一个指定属性的属性(特征)的对象。value 就是其中的特征之一。另外一个 writable 是定义属性的值是否能被改变。

一个通过定义创建的公有字段,而不是通过分配:

class A {
  set prop(value) {
    console.log('SETTER: ' + value);
  }
}
class B extends A {
  prop = 123;
}
assert.equal(new B().prop, 123);

如上,公有字段总是创建属性且忽略 setter

使用定义创建的公有字段的利与弊

反对使用定义公有字段的原因:

  • 如果将创建属性移到构造函数的外面,作为一个字段,这将改变代码的行为。那将是危险的重构。
  • 直到现在,在属性上使用赋值运算符总是触发赋值。

赞成使用定义的原因:

  • 在实体类的顶部声明模型是重写:实体总是被创建,实体独自创建而不是来自于继承。
  • 通过定义创建属性的先例包括:对象字面量中的属性定义和类中的原型声明。

在通常,使用定义声明(而不是分配)是对比利与弊孰轻孰重。

公有实例字段什么时候执行?

公有字段的执行大概遵从下面两条规则:

  • 在基础类中,公有实例字段被立即执行在构造函数之前。
  • 在衍生类,公有实例字段被立即执行在 super() 函数之后。

就像下面的例子:

class SuperClass {
  superProp = console.log('superProp');
  constructor() {
    console.log('super-constructor');
  }
}
class SubClass extends SuperClass {
  subProp = console.log('subProp');
  constructor() {
    console.log('Before super()');
    super();
    console.log('sub-constructor');
  }
}
new SubClass();

// Output:
// 'Before super()'
// 'superProp'
// 'super-constructor'
// 'subProp'
// 'sub-constructor'

字段的初始化范围

在公有实例字段的初始化,this 返回当前实例:

class MyClass {
  prop = this;
}
const instance = new MyClass();
assert.equal(instance.prop, instance);

在公有静态字段的初始化,this 返回当前类:

class MyClass {
  static prop = this;
}
assert.equal(MyClass.prop, MyClass);

另外,super 的运行是可预期的:

class SuperClass {
  getValue() {
    return 123;
  }
}
class SubClass extends SuperClass {
  prop = super.getValue();
}
assert.equal(new SubClass().prop, 123);

公有字段的属性

默认情况下,公有字段是可写的(writable),可枚举(enumerable)且可配置(configurable):

class MyClass {
  static publicStaticField;
  publicInstanceField;
}
assert.deepEqual(
  Object.getOwnPropertyDescriptor(MyClass, 'publicStaticField'),
  {
    value: undefined,
    writable: true,
    enumerable: true,
    configurable: true,
  },
);
assert.deepEqual(
  Object.getOwnPropertyDescriptor(new MyClass(), 'publicInstanceField'),
  {
    value: undefined,
    writable: true,
    enumerable: true,
    configurable: true,
  },
);

想要了解更多属性的 value, writable, enumerableconfigurable,请看“JavaScript for impatient programmers”

了解

类字段(class fields)当前的实现:

了解更多信息,请看the proposal

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

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

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