从普通的ES6类方法中调用静态方法

222

如何调用静态方法是标准的方式?我可以使用constructor, 或者使用类名本身, 但我不喜欢后者,因为它感觉不必要。前者是推荐的方式吗,还是有其他方式?

以下是一个(捏造的)示例:

class SomeObject {
  constructor(n){
    this.n = n;
  }

  static print(n){
    console.log(n);
  }

  printN(){
    this.constructor.print(this.n);
  }
}

9
SomeObject.print 感觉很自然。但是,如果我们在谈论静态方法,那么 this.n 毫无意义,因为此时不存在实例。 - dfsq
4
printN并非静态方法。 - simonzack
你是对的,名字混淆了。 - dfsq
2
我很好奇为什么这个问题没有那么多的赞!难道这不是创建实用函数的常见做法吗? - Thoran
@Thoran 不,这不是常见的做法。更像是一种反模式。通常静态和非静态方法是*截然不同的。统一它们是可以做到的(如所示),但实际上没有任何有用的OOP目的。实例方法使用来自该实例的数据对实例进行操作。仅将调用转发到静态方法并不会做任何这样的事情。此外,静态方法通常是OOP本身崩溃的信号,因为这是模拟非实例功能块的唯一方法。对于这个问题,JS已经有了函数。 - VLAZ
3个回答

254

两种方式都可行,但在涉及覆盖静态方法的继承时,它们会执行不同的操作。选择你期望的行为:

class Super {
  static whoami() {
    return "Super";
  }
  lognameA() {
    console.log(Super.whoami());
  }
  lognameB() {
    console.log(this.constructor.whoami());
  }
}
class Sub extends Super {
  static whoami() {
    return "Sub";
  }
}
new Sub().lognameA(); // Super
new Sub().lognameB(); // Sub

通过类名引用静态属性将会是实际的静态,总是返回相同的值。而使用 this.constructor 将使用动态分派并引用当前实例的类,其中静态属性可能具有继承的值,但也可能被覆盖。

这符合Python的行为,在Python中,您可以选择通过类名或实例self来引用静态属性。

如果您希望静态属性不被覆盖(并且总是引用当前类的静态属性),就像Java一样,请使用显式引用。


你能解释一下构造函数属性和类方法定义之间的区别吗? - Chris
2
@Chris:每个类都是一个构造函数(就像您在没有“class”语法的ES5中所知道的那样),方法的定义没有区别。这只是一个查找方式的问题,可以通过继承的“constructor”属性或直接使用其名称来查找。 - Bergi
另一个例子是PHP的后期静态绑定。这不仅尊重继承,还可以帮助您避免在更改类名时更新代码。 - ricanontherun
1
@ricanontherun 当你更改变量名称时需要更新代码,这并不是不使用名称的论据。此外,重构工具可以自动处理它。 - Bergi
1
如何在 TypeScript 中实现这个?它会报错:Property 'staticProperty' does not exist on type 'Function' - ayZagen
@ayZagen 我不知道,我猜你应该提一个新问题,询问如何为此编写TypeScript类型,并在那里发布不起作用的代码。 - Bergi

85
我在stackoverflow上搜索答案时偶然发现了这个帖子,基本上所有答案都找到了,但仍然很难从中提取要点。
访问方式:
假设有一个类Foo,可能是从其他一些类派生的,也可能有更多的类从它派生而来。然后进行访问:
1. 通过Foo类的静态方法/获取器进行访问: * 可能会重写某些静态方法/获取器: - this.method() - this.property * 可能会重写某些实例方法/获取器: - 设计上不可能
2. 通过Foo类的实例方法/获取器进行访问: * 可能会重写某些静态方法/获取器: - this.constructor.method() - this.constructor.property * 可能会重写某些实例方法/获取器: - this.method() - this.property * 自己的非重写静态方法/获取器: - Foo.method() - Foo.property * 自己的非重写实例方法/获取器: - 不可能(除非使用某些解决方法):
请注意,在使用箭头函数或显式绑定到自定义值的方法/获取器时,使用“this”不起作用。
背景:
1. 当在实例方法/获取器的上下文中时: * this指的是当前实例。 * super基本上指的是相同的实例,但是有点像在访问当前类扩展的一些方法/获取器(通过使用Foo的原型)。 * 它被创建时用于定义实例的类可以通过this.constructor获得。
2. 当在静态方法/获取器的上下文中时,没有“当前实例”,因此: * this可用于直接引用当前类的定义。 * super也不是指某个实例,而是指当前一个扩展了某个类的静态方法/获取器。
结论:
尝试这段代码:
class A {
  constructor( input ) {
    this.loose = this.constructor.getResult( input );
    this.tight = A.getResult( input );
    console.log( this.scaledProperty, Object.getOwnPropertyDescriptor( A.prototype, "scaledProperty" ).get.call( this ) );
  }

  get scaledProperty() {
    return parseInt( this.loose ) * 100;
  }
  
  static getResult( input ) {
    return input * this.scale;
  }
  
  static get scale() {
    return 2;
  }
}

class B extends A {
  constructor( input ) {
    super( input );
    this.tight = B.getResult( input ) + " (of B)";
  }
  
  get scaledProperty() {
    return parseInt( this.loose ) * 10000;
  }

  static get scale() {
    return 4;
  }
}

class C extends B {
  constructor( input ) {
    super( input );
  }
  
  static get scale() {
    return 5;
  }
}

class D extends C {
  constructor( input ) {
    super( input );
  }
  
  static getResult( input ) {
    return super.getResult( input ) + " (overridden)";
  }
  
  static get scale() {
    return 10;
  }
}


let instanceA = new A( 4 );
console.log( "A.loose", instanceA.loose );
console.log( "A.tight", instanceA.tight );

let instanceB = new B( 4 );
console.log( "B.loose", instanceB.loose );
console.log( "B.tight", instanceB.tight );

let instanceC = new C( 4 );
console.log( "C.loose", instanceC.loose );
console.log( "C.tight", instanceC.tight );

let instanceD = new D( 4 );
console.log( "D.loose", instanceD.loose );
console.log( "D.tight", instanceD.tight );


1
拥有未被覆盖的实例方法/获取器/除非使用某些解决方法,否则无法实现。这真是太遗憾了。在我看来,这是ES6+的一个缺点。也许应该更新以允许简单地引用“method”——即“method.call(this)”。比“Foo.prototype.method”更好。Babel等可以使用NFE(命名函数表达式)来实现。 - Roy Tinker
除非“method”未绑定到所需的基础“类”,否则“method.call(this)”可能是一个可行的解决方案,因此无法成为未覆盖的实例方法/获取器。这样可以使用与类无关的方法。尽管如此,我认为当前的设计并不那么糟糕。在从您的基类Foo派生的类的对象上下文中,可能有很好的理由重写实例方法。该覆盖的方法可能有足够的理由调用其“super”实现或不调用。任何情况都是合格的,应遵守。否则,它将导致不良面向对象设计。 - Thomas Urban
尽管ES方法具有OOP语法糖,但它们仍然是“函数”,人们希望将其用作此类函数并引用它们。我对ES类语法的问题在于它没有直接引用当前执行的方法--这曾经可以通过arguments.callee或NFE轻松实现。 - Roy Tinker
听起来像是不好的实践,或者至少是不好的软件设计。我认为这两种观点是相互矛盾的,因为在面向对象编程范式中,我没有看到合适的理由涉及通过引用访问当前调用方法(这不仅仅是通过this可用的上下文)。这听起来像是试图将裸C的指针算术的优势与更高级别的C#混合使用。只是出于好奇:在设计良好的面向对象编程代码中,您会使用arguments.callee做什么? - Thomas Urban
我正在一个使用Dojo类系统构建的大型项目中工作,该系统允许通过this.inherited(currentFn, arguments);调用当前方法的超类实现。在TypeScript中无法直接引用当前正在执行的函数,这使得事情变得有些棘手,因为TypeScript采用ES6的类语法。 - Roy Tinker
我明白了!这主要是由于异构设置需要将旧世界与新世界混合在一起。我自己在一个项目中使用了类似的方法,确实需要参考当前的方法来遍历原型链,以防止在查找下一个重载版本时出现无限回归。然而,在纯ES6项目中,这已经过去了。因此,现在我会坚持在旧项目中使用旧代码,或者尝试从旧式类派生并在这些派生类中开始使用ES6继承处理,但我不知道Dojo类的具体情况,因此无法谈论你的情况。 - Thomas Urban

22

如果您计划进行任何形式的继承,那么我建议使用this.constructor。这个简单的例子应该能说明原因:

class ConstructorSuper {
  constructor(n){
    this.n = n;
  }

  static print(n){
    console.log(this.name, n);
  }

  callPrint(){
    this.constructor.print(this.n);
  }
}

class ConstructorSub extends ConstructorSuper {
  constructor(n){
    this.n = n;
  }
}

let test1 = new ConstructorSuper("Hello ConstructorSuper!");
console.log(test1.callPrint());

let test2 = new ConstructorSub("Hello ConstructorSub!");
console.log(test2.callPrint());
  • test1.callPrint() 将会在控制台上输出 ConstructorSuper Hello ConstructorSuper!
  • test2.callPrint() 将会在控制台上输出 ConstructorSub Hello ConstructorSub!

如果你不显式地重新定义每个引用命名类的函数,那么该命名类将无法很好地处理继承。以下是一个例子:

class NamedSuper {
  constructor(n){
    this.n = n;
  }

  static print(n){
    console.log(NamedSuper.name, n);
  }

  callPrint(){
    NamedSuper.print(this.n);
  }
}

class NamedSub extends NamedSuper {
  constructor(n){
    this.n = n;
  }
}

let test3 = new NamedSuper("Hello NamedSuper!");
console.log(test3.callPrint());

let test4 = new NamedSub("Hello NamedSub!");
console.log(test4.callPrint());
  • test3.callPrint()将在控制台记录NamedSuper Hello NamedSuper!
  • test4.callPrint()将在控制台记录NamedSuper Hello NamedSub!

在Babel REPL中查看上述所有内容

从中可以看出,test4仍然认为它处于超类中;在这个示例中似乎不是很重要,但如果您试图引


3
静态函数不是被重写的成员方法。通常你不会尝试在静态上下文中引用任何被重写的内容。 - Bergi
2
@Bergi 我不确定我理解你指出的是什么,但我遇到的一个具体情况是MVC模型水合模式。扩展模型的子类可能希望实现静态水合函数。然而,当这些硬编码时,只会返回基本模型实例。这是一个非常具体的例子,但许多依赖于具有注册实例的静态集合的模式都会受到影响。一个重要的免责声明是,我们在这里尝试模拟经典继承,而不是原型继承...而这并不流行 :P - Andrew Odri
1
是的,正如我在自己的回答中得出的结论,这甚至在“经典”继承中也没有一致地解决 - 有时您可能需要覆盖,有时不需要。 我评论的第一部分指向静态类函数,我并未考虑它们为“成员”。最好忽略它 :-) - Bergi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接