[译]ES2022特性:类静态初始化块

3,459 阅读3分钟

Ron Buckton向ECMAScript提交的“Class static initialization blocks” 提案已经进入“阶段4”,预期将进入ECMAScript 2022。

在JavaScript中,要创建类的实例,有两种构造可以使用:

  • 字段:创建(并可选地初始化)实例属性。
  • 构造器:代码块,在实例创建完成之前执行。

至于创建类的静态部分,则只有静态字段。这里提到的ECMAScript提案给类引入了静态初始化块。简单地说,静态初始化块之于静态类,就相当于构造器之于实例。


目录:


为什么类中需要静态块?

在创建静态字段时,通常可以使用外部函数:

class Translator {
  static translations = {
    yes: 'ja',
    no: 'nein',
    maybe: 'vielleicht',
  };
  static englishWords = extractEnglish(translations);
  static germanWords = extractGerman(translations);
}
function extractEnglish(translations) {
  return Object.keys(translations);
}
function extractGerman(translations) {
  return Object.values(translations);
}

在这里使用外部函数 extractEnglish()extractGerman() 之所以行得通,是因为可以看到它们在类的内部被调用,而且它们与类是完全独立的。

如果想同时创建两个静态字段,代码看起来就没那么清晰了:

class Translator {
  static translations = {
    yes: 'ja',
    no: 'nein',
    maybe: 'vielleicht',
  };
  static englishWords = [];
  static germanWords = [];
  static _ = initializeTranslator(); // (A)
}
function initializeTranslator() {
  for (const [english, german] of Object.entries(Translator.translations)) {
    Translator.englishWords.push(english);
    Translator.germanWords.push(german);
  }
}

这次的代码有几个问题:

  • 调用 initializeTranslator() 是额外的步骤,这个步骤要么必须等到类初始化之后执行,要么就得通过一个变通方案来执行(A行)。
  • initializeTranslator() 不能访问Translator的私有数据。

使用提案建议的静态块(A行),代码会更容易理解。

class Translator {
  static translations = {
    yes: 'ja',
    no: 'nein',
    maybe: 'vielleicht',
  };
  static englishWords = [];
  static germanWords = [];
  static { // (A)
    for (const [english, german] of Object.entries(translations)) {
      this.englishWords.push(english);
      this.germanWords.push(german);
    }
  }
}

一个更复杂的例子

在JavaScript中要实现枚举,一种方式是通过超类Enum,外加一些辅助(更完善的实现可以参考 这个库enumify ):

class Enum {
  static collectStaticFields() {
    // 静态方法不能枚举,因此忽略
    this.enumKeys = Object.keys(this);
  }
}
class ColorEnum extends Enum {
  static red = Symbol('red');
  static green = Symbol('green');
  static blue = Symbol('blue');
  static _ = this.collectStaticFields(); // (A)

  static logColors() {
    for (const enumKey of this.enumKeys) { // (B)
      console.log(enumKey);
    }
  }
}
ColorEnum.logColors();

// Output:
// 'red'
// 'green'
// 'blue'

我们想收集静态字段,因而可以迭代枚举条目的键(B行)。这一步是在创建所有静态字段之后执行的。这里同样也用到了一个变通方案(A行)。同样,使用静态块会让代码更清晰。

细节

静态块的细节相对符合逻辑(相较于针对实例成员更复杂的规则而言):

  • 每个类可以有多个静态块。
  • 静态块的执行与静态字段初始化程序的执行是交错的。
  • 超类的静态成员在子类静态成员之前执行。

以下代码演示了这些规则:

class SuperClass {
  static superField1 = console.log('superField1');
  static {
    assert.equal(this, SuperClass);
    console.log('static block 1 SuperClass');
  }
  static superField2 = console.log('superField2');
  static {
    console.log('static block 2 SuperClass');
  }
}

class SubClass extends SuperClass {
  static subField1 = console.log('subField1');
  static {
    assert.equal(this, SubClass);
    console.log('static block 1 SubClass');
  }
  static subField2 = console.log('subField2');
  static {
    console.log('static block 2 SubClass');
  }
}

// Output:
// 'superField1'
// 'static block 1 SuperClass'
// 'superField2'
// 'static block 2 SuperClass'
// 'subField1'
// 'static block 1 SubClass'
// 'subField2'
// 'static block 2 SubClass'

支持类静态块的引擎

  • V8:在v9.4.146中不加标签(source
  • SpiderMonkey:v92要加标签,计划在v93去掉标签(source
  • TypeScript:v4.4(source

结论

类静态块是个相对简单的特性,让类的静态特性更趋完善。粗略地讲,它是实例构造器的静态版。其主要用途是创建多个静态字段。