web-dev-qa-db-fra.com

ES6: constructeur de classe d'appel sans nouveau mot clé

Compte tenu d'un cours simple

class Foo {
  constructor(x) {
    if (!(this instanceof Foo)) return new Foo(x);
    this.x = x;
  }
  hello() {
    return `hello ${this.x}`;
  }
}

Est-il possible d'appeler le constructeur de classe sans le mot clé new?

L'utilisation devrait permettre

(new Foo("world")).hello(); // "hello world"

Ou

Foo("world").hello();       // "hello world"

Mais ce dernier échoue avec

Cannot call a class as a function
66
user633183

Les classes ont un "corps de classe" qui est un constructeur.
Si vous utilisez une fonction interne constructor(), celle-ci aurait également le même corps de classe et serait appelée lorsque la classe est appelée. Une classe est donc toujours un constructeur.

Les constructeurs requièrent l’utilisation de l’opérateur new pour créer une nouvelle instance. Par conséquent, l’appel d’une classe sans l’opérateur new entraîne une erreur, car requis pour le constructeur de classe pour créer une nouvelle instance.

Le message d'erreur est également assez spécifique et correct

TypeError: les constructeurs de classe ne peuvent pas être appelés sans 'new'

Vous pourriez;

  • soit utiliser une fonction régulière au lieu d'une classe1.
  • Appelez toujours la classe avec new.
  • Appelez la classe à l'intérieur d'une fonction habituelle d'habillage, en utilisant toujours new pour bénéficier des avantages des classes, mais la fonction d'habillage peut toujours être appelée avec et sans l'opérateur new.2.

1)

function Foo(x) {
    if (!(this instanceof Foo)) return new Foo(x);
    this.x = x;
    this.hello = function() {
        return this.x;
    }
}

2)

class Foo {
    constructor(x) {
        this.x = x;
    }
    hello() {
        return `hello ${this.x}`;
    }
}

var _old = Foo;
Foo = function(...args) { return new _old(...args) };
37
adeneo

Comme d'autres l'ont souligné, la spécification ES2015 stipule strictement que cet appel doit renvoyer TypeError, mais fournit en même temps une fonctionnalité qui peut être utilisée pour obtenir exactement le résultat souhaité, à savoir Proxies .

Les procurations nous permettent de virtualiser sur un concept d'objet. Par exemple, ils peuvent être utilisés pour modifier certains comportements d'objets particuliers sans rien affecter d'autre.

Dans votre cas d'utilisation spécifique class Foo Est Function object Qui peut être appelé - cela signifie normalement que le corps de cette fonction sera exécuté. Mais cela peut être changé avec Proxy:

const _Foo = new Proxy(Foo, {
  // target = Foo
  apply (target, thisArg, argumentsList) {
    return new target(...argumentsList);
  }
});

_Foo("world").hello(); 
const f = _Foo("world");
f instanceof Foo; // true
f instanceof _Foo; // true

(Notez que _Foo Est maintenant la classe que vous voulez exposer, donc les identifiants devraient probablement être l'inverse.)

S'il est exécuté par un navigateur prenant en charge les mandataires, l'appel de _Foo(...) exécutera désormais la fonction apply trap à la place du constructeur d'origine.

En même temps, cette "nouvelle" classe _Foo Est indiscernable de l'originale Foo (hormis le fait de pouvoir l'appeler comme une fonction normale). De même, il n'y a pas de différence permettant de distinguer un objet créé avec Foo et _Foo.

Le plus gros inconvénient de ceci est que il ne peut pas être transpillé ou pollyfilled , mais reste sa solution viable pour que la classe de type Scala soit appliquée à JS à l'avenir.

29
Adam Ples

Voici un motif que j'ai rencontré qui m'aide vraiment. Il n'utilise pas class, mais il ne nécessite pas non plus l'utilisation de new. Win/Win.

const Foo = x => ({
  x,
  hello: () => `hello ${x}`,
  increment: () => Foo(x + 1),
  add: ({x: y}) => Foo(x + y)
})

console.log(Foo(1).x)                   // 1
console.log(Foo(1).hello())             // hello 1
console.log(Foo(1).increment().hello()) // hello 2
console.log(Foo(1).add(Foo(2)).hello()) // hello 3
21
user633183

Non, ce n'est pas possible. Les constructeurs créés avec le mot clé class ne peuvent être construits qu'avec new, s'ils sont [[appel]] ed sans toujours throw un TypeError1 (et il n'y a même pas moyen de détecter cela de l'extérieur).
1: Je ne suis pas sûr que les transpilers obtiennent ce droit

Vous pouvez utiliser une fonction normale comme solution de contournement, cependant:

class Foo {
  constructor(x) {
    this.x = x;
  }
  hello() {
    return `hello ${this.x}`;
  }
}
{
  const _Foo = Foo;
  Foo = function(...args) {
    return new _Foo(...args);
  };
  Foo.prototype = _Foo.prototype;
}

Avertissement: instanceof et étendre Foo.prototype Fonctionnent normalement, Foo.length Ne le fait pas, .constructor Et les méthodes statiques ne le font pas mais peuvent être corrigées en ajoutant Foo.prototype.constructor = Foo; et Object.setPrototypeOf(Foo, _Foo) si nécessaire.

Pour sous-classer Foo (et non pas _Foo) Avec class Bar extends Foo …, Vous devez utiliser return Reflect.construct(_Foo, args, new.target) au lieu de l'appel new _Foo. Le sous-classement dans le style ES5 (avec Foo.call(this, …)) n'est pas possible.

12
Bergi
class MyClass {

  constructor(param) {
     // ...
  }

  static create(param) {
    return new MyClass(param);
  }

  doSomething() {
    // ...
  }

}

MyClass.create('Hello World').doSomething();

Est-ce que c'est ce que tu veux?

Si vous avez besoin de logique lors de la création d'une nouvelle instance de MyClass, il pourrait être utile d'implémenter un "CreationStrategy" pour dépasser la logique:

class MyClassCreationStrategy {

  static create(param) {
    let instance = new MyClass();
    if (!param) {
      // eg. handle empty param
    }

    instance.setParam(param);
    return instance;
  }

}

class DefaultCreationStrategy {

  static create(classConstruct) { 
    return new classConstruct(); 
  }

}

MyClassCreationStrategy.create(param).doSomething();
DefaultCreationStrategy.create(MyClass).doSomething();
6
Tim

je viens de faire ce module NPM pour vous;)

https://www.npmjs.com/package/classy-decorator

import classy from "classy-decorator";

@classy()
class IamClassy {
    constructor() {
        console.log("IamClassy Instance!");
    }
}

console.log(new IamClassy() instanceof IamClassy);  // true 

console.log(IamClassy() instanceof IamClassy);  // true 
6

Creusez celui-ci dans le brouillon

Les constructeurs définis à l'aide de la syntaxe de définition de classe jettent lorsqu'ils sont appelés en tant que fonctions

Donc, je suppose que ce n'est pas possible avec les cours.

5
Joseph

D'accord, j'ai une autre réponse ici, et je pense que celle-ci est assez novatrice.

Fondamentalement, le problème avec quelque chose de similaire à la réponse de Naomik est que vous créez des fonctions chaque fois que vous chaînez des méthodes.

EDIT: Cette solution partage le même problème, cependant, cette réponse est laissée à des fins éducatives.

Je vous propose donc ici un moyen de lier simplement de nouvelles valeurs à vos méthodes, qui sont essentiellement des fonctions indépendantes. Cela offre l’avantage supplémentaire de pouvoir importer des fonctions de différents modules dans le nouvel objet construit.

Ok, alors c'est parti.

const assoc = (prop, value, obj) => 
  Object.assign({},obj,{[prop]: value})

const reducer = ( $values, accumulate, [key,val] ) => assoc( key, val.bind( undefined,...$values ), accumulate )

const bindValuesToMethods = ( $methods, ...$values ) => 
  Object.entries( $methods ).reduce( reducer.bind( undefined, ...$values), {} )

const prepareInstance = (instanceMethods, staticMethods = ({}) ) => Object.assign(
  bindValuesToMethods.bind( undefined, instanceMethods ),
  staticMethods
)

// Let's make our class-like function

const RightInstanceMethods = ({
  chain: (x,f) => f(x),
  map: (x,f) => Right(f(x)),
  fold: (x,l,r) => r(x),
  inspect: (x) => `Right(${x})`
})

const RightStaticMethods = ({
  of: x => Right(x)
})

const Right = prepareInstance(RightInstanceMethods,RightStaticMethods)

Maintenant tu peux faire

Right(4)
  .map(x=>x+1)
  .map(x=>x*2)
  .inspect()

Vous pouvez aussi faire

Right.of(4)
  .map(x=>x+1)
  .map(x=>x*2)
  .inspect()

Vous avez également l'avantage de pouvoir exporter des modules en tant que tels.

export const Right = prepareInstance(RightInstanceMethods,RightStaticMethods)

Tant que vous n'obtenez pas ClassInstance.constructor vous avez FunctorInstance.name (notez qu'il peut être nécessaire de remplir polyfill Function.name et/ou ne pas utiliser de fonction de flèche pour l'exportation afin d'assurer la compatibilité du navigateur avec Function.name _ fins)

export function Right(...args){
  return prepareInstance(RightInstanceMethods,RightStaticMethods)(...args)
}

PS - Nouvelles suggestions de noms pour prepareInstance, voir Gist.

https://Gist.github.com/babakness/56da19ba85e0eaa43ae5577bc0064456

2
Babakness

J'ai eu des problèmes lors de l'extension de classes converties avec la fonction de transformation mentionnée dans d'autres réponses. Le problème semble être que le noeud (à partir de la v9.4.0) ne prend pas correctement en charge l’opérateur de propagation d’argument ((...args) =>).

Cette fonction basée sur la sortie transcrite du classy-decorator (mentionné dans autre réponse ) fonctionne pour moi et ne nécessite pas de support pour les décorateurs ni pour l'opérateur de propagation d'argument.

// function that calls `new` for you on class constructors, simply call
// YourClass = bindNew(YourClass)
function bindNew(Class) {
  function _Class() {
    for (
      var len = arguments.length, rest = Array(len), key = 0;
      key < len;
      key++
    ) {
      rest[key] = arguments[key];
    }

    return new (Function.prototype.bind.apply(Class, [null].concat(rest)))();
  }
  _Class.prototype = Class.prototype;
  return _Class;
}

Usage:

class X {}
X = bindNew(X);

// or

const Y = bindNew(class Y {});

const x = new X();
const x2 = X(); // woohoo

x instanceof X; // true
x2 instanceof X; // true

class Z extends X {} // works too

En prime, TypeScript (avec la sortie "es5") semble bien fonctionner avec l'ancienne astuce instanceof (enfin, il ne sera pas typecheck s'il est utilisé sans new mais il fonctionne quand même):

class X {
  constructor() {
    if (!(this instanceof X)) {
      return new X();
    }
  }
}

parce qu'il compile jusqu'à:

var X = /** @class */ (function () {
    function X() {
        if (!(this instanceof X)) {
            return new X();
        }
    }
    return X;
}());
2
kasbah

Voici un endroit où vous pouvez utiliser un 'constructeur sécurisé de portée' Observez ce code:

function Student(name) {
  if(this instanceof Student) {
    this.name = name;
  } else {
    return new Student(name);
  }
}

Vous pouvez maintenant créer un objet Student sans utiliser de nouveau, comme suit:

var stud1 = Student('Kia');
2
user2922935

Le constructeur de classe d'appel peut être utile manuellement lors du refactoring de code (avoir des parties du code dans ES6, d'autres parties en tant que fonction et définition du prototype)

Je me suis retrouvé avec un passe-partout petit mais utile, découpant le constructeur en une autre fonction. Période.

 class Foo {
  constructor() {
    //as i will not be able to call the constructor, just move everything to initialize
    this.initialize.apply(this, arguments)
  }

  initialize() {
    this.stuff = {};
    //whatever you want
  }
 }

  function Bar () {
    Foo.prototype.initialize.call(this); 
  } 
  Bar.prototype.stuff = function() {}
2
131

J'ajoute ceci pour faire suite à un commentaire de naomik et en utilisant la méthode illustrée par Tim et Bergi. Je vais également suggérer une fonction of à utiliser comme cas général.

Pour faire cela de manière fonctionnelle ET utiliser l'efficacité des prototypes (ne pas recréer toutes les méthodes à chaque fois qu'une nouvelle instance est créée), on pourrait utiliser ce modèle.

const Foo = function(x){ this._value = x ... }
Foo.of = function(x){ return new Foo(x) }
Foo.prototype = {
  increment(){ return Foo.of(this._value + 1) },
  ...
}

Veuillez noter que ceci est conforme à fantasy-land JS specs

https://github.com/fantasyland/fantasy-land#of-method

Personnellement, j'estime qu'il est plus propre d'utiliser la syntaxe de classe ES6

class Foo {
  static of(x) { new Foo(x)}
  constructor(x) { this._value = x }
  increment() { Foo.of(this._value+1) }
}

Maintenant, on pourrait envelopper dans une fermeture en tant que telle

class Foo {
  static of(x) { new _Foo(x)}
  constructor(x) { this._value = x }
  increment() { Foo.of(this._value+1) }
}


function FooOf (x) {

    return Foo.of(x)

}

Ou renommez FooOf et Foo comme vous le souhaitez, c’est-à-dire que la classe pourrait être FooClass et la fonction simplement Foo, etc.

Cela vaut mieux que de placer la classe dans la fonction car la création de nouvelles instances ne nous alourdit pas avec la création de nouvelles classes.

Une autre méthode consiste à créer une fonction of

const of = (classObj,...args) => (
  classObj.of 
    ? classObj.of(value) 
    : new classObj(args)
)

Et ensuite, faites quelque chose comme of(Foo,5).increment()

0
Babakness

L'appel du constructeur de la classe sans le mot clé new n'est pas possible.

Le message d'erreur est assez spécifique.

Voir un article de blog sur 2ality et le spec :

However, you can only invoke a class via new, not via a function call (Sect. 9.2.2 in the spec):

    > Point()
    TypeError: Classes can’t be function-called
0