Chame métodos estáticos de métodos regulares da classe ES6

175

Qual é a maneira padrão de chamar métodos estáticos? Posso pensar em usar constructorou usar o nome da classe em si, não gosto desta última, pois ela não parece necessária. O primeiro é o caminho recomendado ou existe algo mais?

Aqui está um exemplo (artificial):

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

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

  printN(){
    this.constructor.print(this.n);
  }
}
simonzack
fonte
8
SomeObject.printparece natural. Mas por this.ndentro não faz sentido, pois não há exemplo, se estamos falando de métodos estáticos.
dfsq
3
O @dfsq printNnão é estático.
21615 Simonzack
Você está certo, nomes confusos.
dfsq
1
Estou curioso para saber por que essa pergunta não tem tantos votos positivos! Esta não é uma prática comum para a criação de funções utilitárias?
Thoran

Respostas:

210

Ambas as formas são viáveis, mas fazem coisas diferentes quando se trata de herança com um método estático substituído. Escolha aquele cujo comportamento você espera:

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

A referência à propriedade estática por meio da classe será realmente estática e fornecerá constantemente o mesmo valor. Usar this.constructorvez usará o despacho dinâmico e se referirá à classe da instância atual, onde a propriedade estática pode ter o valor herdado, mas também pode ser substituída.

Isso corresponde ao comportamento do Python, onde você pode optar por se referir às propriedades estáticas pelo nome da classe ou pela instância self .

Se você espera que as propriedades estáticas não sejam substituídas (e sempre consulte a da classe atual), como em Java , use a referência explícita.

Bergi
fonte
você pode explicar a definição do método construtor vs classe?
Chris
2
@ Chris: Toda classe é uma função construtora (assim como você conhece no ES5 sem classsintaxe), não há diferença na definição do método. É apenas uma questão de como você a pesquisa, através da constructorpropriedade herdada ou diretamente pelo nome.
Bergi
Outro exemplo são as Ligações estáticas tardias do PHP . This.constructor não apenas respeita a herança, mas também ajuda a evitar a necessidade de atualizar o código se você alterar o nome da classe.
Ricanontherun
@ricanontherun Ter que atualizar o código ao alterar nomes de variáveis ​​não é um argumento contra o uso de nomes. As ferramentas de refatoração também podem fazê-lo automaticamente de qualquer maneira.
Bergi 19/07/2018
Como implementar isso em texto datilografado? Dá o erroProperty 'staticProperty' does not exist on type 'Function'
ayZagen 28/03/19
71

Eu tropecei sobre esta discussão em busca de resposta para um caso semelhante. Basicamente, todas as respostas são encontradas, mas ainda é difícil extrair o essencial delas.

Tipos de acesso

Suponha uma classe Foo provavelmente derivada de outras classes com provavelmente mais classes derivadas dela.

Então acessando

  • do método estático / getter de Foo
    • algum método / getter estático provavelmente substituído :
      • this.method()
      • this.property
    • algum método de instância provavelmente substituído / getter:
      • impossível por design
    • próprio método estático não substituído / getter:
      • Foo.method()
      • Foo.property
    • próprio método de instância não substituído / getter:
      • impossível por design
  • do método de instância / getter de Foo
    • algum método / getter estático provavelmente substituído :
      • this.constructor.method()
      • this.constructor.property
    • algum método de instância provavelmente substituído / getter:
      • this.method()
      • this.property
    • próprio método estático não substituído / getter:
      • Foo.method()
      • Foo.property
    • próprio método de instância não substituído / getter:
      • não é possível por intenção, a menos que você use alguma solução alternativa :
        • Foo.prototype.method.call( this )
        • Object.getOwnPropertyDescriptor( Foo.prototype,"property" ).get.call(this);

Lembre-se de que o uso thisnão está funcionando dessa maneira ao usar funções de seta ou ao chamar métodos / getters explicitamente vinculados ao valor personalizado.

fundo

  • Quando no contexto do método ou getter de uma instância
    • this está se referindo à instância atual.
    • super está basicamente se referindo à mesma instância, mas abordando métodos e getters escritos no contexto de alguma classe atual que está se estendendo (usando o protótipo do protótipo de Foo).
    • A definição da classe da instância usada na criação está disponível por this.constructor.
  • Quando no contexto de um método estático ou getter, não há "instância atual" por intenção e, portanto,
    • this está disponível para se referir diretamente à definição da classe atual.
    • super também não se refere a alguma instância, mas a métodos estáticos e getters escritos no contexto de alguma classe atual que está sendo estendida.

Conclusão

Tente este código:

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 );

Thomas Urban
fonte
1
Own non-overridden instance method/getter / not possible by intention unless using some workaround--- Isso é uma pena. Na minha opinião, esta é uma deficiência do ES6 +. Talvez deva ser atualizado para permitir simplesmente se referir a method- ie method.call(this). Melhor que Foo.prototype.method. Babel / etc. poderia implementar usando um NFE (expressão da função nomeada).
Roy Tinker
method.call( this )é uma solução provável, exceto a que methodnão está vinculada à "classe" base desejada e, portanto, deixa de ser um método / getter de instância não substituída . Sempre é possível trabalhar com métodos independentes de classe dessa maneira. No entanto, não acho que o design atual seja tão ruim assim. No contexto de objetos de classe derivados de sua classe base Foo, pode haver boas razões para substituir um método de instância. Esse método substituído pode ter boas razões para invocar sua superimplementação ou não. Qualquer um dos casos é elegível e deve ser obedecido. Caso contrário, terminaria com um design ruim de POO.
Thomas Urban
Apesar do açúcar OOP, os métodos ES ainda são funções , e as pessoas vão querer usá-lo e referenciá-lo como tal. Meu problema com a sintaxe da classe ES é que ela não fornece referência direta ao método atualmente em execução - algo que costumava ser fácil via arguments.calleeou um NFE.
Roy Tinker
Parece uma má prática ou, pelo menos, um mau design de software. Eu consideraria os dois pontos de vista contrários um ao outro, pois não vejo razão elegível no contexto do paradigma OOP que envolve o acesso ao método atualmente invocado por referência (que não é apenas o seu contexto, como está disponível via this). Parece tentar combinar os benefícios da aritmética dos ponteiros do C simples com o C # de nível superior. Apenas por curiosidade: o que você usaria arguments.calleeno código OOP limpo?
Thomas Urban
Estou trabalhando em um grande projeto construído com o sistema de classes do Dojo, que permite chamar a (s) implementação (ões) da superclasse (s) do método atual via this.inherited(currentFn, arguments);- onde currentFné uma referência à função atualmente em execução. Não poder fazer referência diretamente à função atualmente em execução está tornando-a um pouco complicada no TypeScript, que retira sua sintaxe de classe do ES6.
Roy Tinker
20

Se você planeja fazer algum tipo de herança, eu recomendaria this.constructor. Este exemplo simples deve ilustrar o motivo:

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()registrará ConstructorSuper Hello ConstructorSuper!no console
  • test2.callPrint()registrará ConstructorSub Hello ConstructorSub!no console

A classe nomeada não tratará bem da herança, a menos que você redefina explicitamente todas as funções que fazem referência à classe nomeada. Aqui está um exemplo:

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()registrará NamedSuper Hello NamedSuper!no console
  • test4.callPrint()registrará NamedSuper Hello NamedSub!no console

Veja todas as opções acima em execução no Babel REPL .

Você pode ver disso que test4ainda acha que está na super classe; neste exemplo, pode não parecer muito importante, mas se você estiver tentando fazer referência a funções-membro que foram substituídas ou a novas variáveis-membro, estará com problemas.

Andrew Odri
fonte
3
Mas função estática não são métodos de membro substituídos? Geralmente você está tentando não fazer referência a qualquer coisa substituída estaticamente.
Bergi
1
@ Bergi Não sei se entendi o que você está apontando, mas um caso específico que me deparei seria com os padrões de hidratação do modelo MVC. As subclasses que estendem um modelo podem querer implementar uma função de hidrato estático. No entanto, quando estes são codificados, as instâncias do modelo base são retornadas apenas sempre. Este é um exemplo bastante específico, mas muitos padrões que dependem de uma coleção estática de instâncias registradas seriam afetados por isso. Um grande aviso é que estamos tentando simular herança clássica aqui, ao invés de herança prototípica ... E isso não é popular: P
Andrew Odri
Sim, como concluí agora em minha própria resposta, isso nem mesmo é resolvido de maneira consistente na herança "clássica" - às vezes você pode querer substituições, às vezes não. A primeira parte do meu comentário apontou para funções de classe estática, que eu não considerava "membros". Melhor ignorá-lo :-)
Bergi