web-dev-qa-db-fra.com

Comment vérifiez-vous la différence entre une classe et une fonction ECMAScript 6?

Dans ECMAScript 6, le typeof des classes est, selon la spécification, 'function'.

Cependant, selon la spécification, vous n'êtes pas autorisé à appeler l'objet créé via la syntaxe de classe comme un appel de fonction normal. En d'autres termes, vous devez utiliser le mot clé new sinon une TypeError est levée.

TypeError: Classes can’t be function-called

Donc, sans utiliser try catch, qui serait très moche et qui détruirait les performances, comment pouvez-vous vérifier si une fonction provenait de la syntaxe class ou de la syntaxe function?

35
Moncader

Je pense que le moyen le plus simple de vérifier si la fonction est la classe ES6 est de vérifier le résultat de la méthode .toString() . Selon la es2015 spec :

La représentation sous forme de chaîne doit avoir la syntaxe d'une FunctionDeclaration FunctionExpression, GeneratorDeclaration, GeneratorExpression, ClassDeclaration, ClassExpression, ArrowFunction, MethodDefinition ou GeneratorMethod selon les caractéristiques réelles de l'objet

La fonction de vérification semble donc assez simple:

function isClass(func) {
  return typeof func === 'function' 
    && /^class\s/.test(Function.prototype.toString.call(func));
}
35
alexpods

J'ai fait quelques recherches et j'ai découvert que l'objet prototype [ spéc 19.1.2.16 ] des classes ES6 semble être non inscriptible, non énumérable , non configurable.

Voici un moyen de vérifier:

class F { }

console.log(Object.getOwnPropertyDescriptor(F, 'prototype'));
// {"value":{},"writable":false,"enumerable":false,"configurable":false

Une fonction régulière par défaut est inscriptible, non énumérable, non configurable.

function G() { }

console.log(Object.getOwnPropertyDescriptor(G, 'prototype'));
// {"value":{},"writable":true,"enumerable":false,"configurable":false}

ES6 Fiddle: http://www.es6fiddle.net/i7d0eyih/

Ainsi, un descripteur de classe ES6 aura toujours ces propriétés définies sur false et générera une erreur si vous essayez de définir les descripteurs.

// Throws Error
Object.defineProperty(F, 'prototype', {
  writable: true
});

Cependant, avec une fonction régulière, vous pouvez toujours définir ces descripteurs.

// Works
Object.defineProperty(G, 'prototype', {
  writable: false
});

Il n'est pas très courant que les descripteurs soient modifiés sur des fonctions régulières, vous pouvez donc probablement l'utiliser pour vérifier s'il s'agit d'une classe ou non, mais bien sûr, ce n'est pas une vraie solution.

La méthode @alexpods de filtrage de la fonction et de vérification du mot-clé class est probablement la meilleure solution pour le moment.

12
Miguel Mota

Ran some performance benchmarks sur les différentes approches mentionnées dans ce fil, voici un aperçu:


Native Class - Props Method (le plus rapide de 56x sur les grands exemples et de 15x sur les exemples triviaux):

function isNativeClass (thing) {
    return typeof thing === 'function' && thing.hasOwnProperty('prototype') && !thing.hasOwnProperty('arguments')
}

Ce qui fonctionne parce que ce qui suit est vrai:

> Object.getOwnPropertyNames(class A {})
[ 'length', 'name', 'prototype' ]
> Object.getOwnPropertyNames(class A { constructor (a,b) {} })
[ 'length', 'name', 'prototype' ]
> Object.getOwnPropertyNames(class A { constructor (a,b) {} a (b,c) {} })
[ 'length', 'name', 'prototype' ]
> Object.getOwnPropertyNames(function () {})
[ 'length', 'name', 'arguments', 'caller', 'prototype' ]
> Object.getOwnPropertyNames(() => {})
> [ 'length', 'name' ]

Native Class - String Method (plus rapide que la méthode regex d'environ 10%):

/**
 * Is ES6+ class
 * @param {any} value
 * @returns {boolean}
 */
function isNativeClass (value /* :mixed */ ) /* :boolean */ {
    return typeof value === 'function' && value.toString().indexOf('class') === 0
}

Cela peut également être utile pour déterminer une classe conventionnelle:

// Character positions
const INDEX_OF_FUNCTION_NAME = 9  // "function X", X is at index 9
const FIRST_UPPERCASE_INDEX_IN_ASCII = 65  // A is at index 65 in ASCII
const LAST_UPPERCASE_INDEX_IN_ASCII = 90   // Z is at index 90 in ASCII

/**
 * Is Conventional Class
 * Looks for function with capital first letter MyClass
 * First letter is the 9th character
 * If changed, isClass must also be updated
 * @param {any} value
 * @returns {boolean}
 */
function isConventionalClass (value /* :any */ ) /* :boolean */ {
    if ( typeof value !== 'function' )  return false
    const c = value.toString().charCodeAt(INDEX_OF_FUNCTION_NAME)
    return c >= FIRST_UPPERCASE_INDEX_IN_ASCII && c <= LAST_UPPERCASE_INDEX_IN_ASCII
}

Je recommanderais également de vérifier mon typechecker package qui inclut les cas d'utilisation pour ce qui précède - via la méthode isNativeClass, isConventionalClass, et une méthode isClass qui vérifie les deux types.

10
balupton

Étant donné que les réponses existantes abordent ce problème du point de vue de l'environnement ES5, j'ai pensé qu'il valait la peine d'offrir une réponse du point de vue ES2015 +; la question d'origine ne précise pas et aujourd'hui beaucoup de gens n'ont plus besoin de transpiler les classes, ce qui modifie un peu la situation.

En particulier, je voulais noter qu'il est possible de répondre définitivement à la question "cette valeur peut-elle être construite?" Certes, cela n’est généralement pas utile en soi; les mêmes problèmes fondamentaux continuent d'exister si vous avez besoin de savoir si une valeur peut être appelée.

Quelque chose est-il constructible?

Pour commencer, je pense que nous devons clarifier une terminologie car demander si une valeur est un constructeur peut signifier plus d'une chose:

  1. Littéralement, cette valeur a-t-elle un emplacement [[construct]]? Si c'est le cas, il est constructible. Si ce n'est pas le cas, il n'est pas constructible.
  2. Cette fonction était-elle destinée à être construite? Nous pouvons produire quelques négatifs: les fonctions qui ne peuvent pas être construites n'étaient pas destinées à être construites. Mais nous ne pouvons pas non plus dire (sans recourir à des contrôles heuristiques) si une fonction qui est constructible n'était pas destinée à être utilisée comme constructeur.

Ce qui rend 2 sans réponse, c'est que les fonctions créées avec le mot clé function seul sont à la fois constructibles et appelables, mais ces fonctions sont souvent destinées à une seule de ces fins. Comme certains l'ont mentionné, 2 est aussi une question louche - cela revient à demander "à quoi pensait l'auteur quand ils ont écrit ceci?" Je ne pense pas que l'IA soit encore là :) Alors que dans un monde parfait, peut-être que tous les auteurs réserveraient PascalCase aux constructeurs (voir la fonction isConventionalClass de balupton), en pratique, il ne serait pas inhabituel de rencontrer des faux positifs/négatifs avec ce test.

Concernant la première version de cette question, oui, on peut savoir si une fonction est constructible. La chose évidente à faire est d'essayer de le construire. Ce n'est pas vraiment acceptable car nous ne savons pas si cela aurait des effets secondaires - il semble acquis que nous ne savons rien de la nature de la fonction, car si nous le faisions, nous n'aurions pas besoin cette vérification). Heureusement, il existe un moyen de construire un constructeur sans vraiment le construire:

const isConstructable = fn => {
  try {
    new new Proxy(fn, { construct: () => ({}) });
    return true;
  } catch (err) {
    return false;
  }
};

Le gestionnaire construct Proxy peut remplacer la [[construction]] d'une valeur proxy, mais il ne peut pas rendre constructible une valeur non constructible. Nous pouvons donc "simuler instantanément" l'entrée pour tester si cela échoue. Notez que le piège de construction doit renvoyer un objet.

isConstructable(class {});                      // true
isConstructable(class {}.bind());               // true
isConstructable(function() {});                 // true
isConstructable(function() {}.bind());          // true
isConstructable(() => {});                      // false
isConstructable((() => {}).bind());             // false
isConstructable(async () => {});                // false
isConstructable(async function() {});           // false
isConstructable(function * () {});              // false
isConstructable({ foo() {} }.foo);              // false
isConstructable(URL);                           // true

Notez que les fonctions fléchées, les fonctions asynchrones, les générateurs et les méthodes ne sont pas à double fonction comme le sont les déclarations et expressions de fonction "héritées". Ces fonctions n'ont pas de créneau [[construct]] (je pense que peu de gens réalisent que la syntaxe de la "méthode raccourcie" fait quelque chose - ce n'est pas seulement du sucre).

Donc, pour récapituler, si votre question est vraiment "est-ce constructible", ce qui précède est concluant. Malheureusement, rien d'autre ne le sera.

Est-ce que quelque chose peut être appelé?

Nous devrons clarifier la question à nouveau, car si nous sommes très littéraux, le test suivant fonctionne réellement *:

const isCallable = fn => typeof fn === 'function';

Cela est dû au fait qu'ES ne vous permet pas actuellement de créer une fonction sans un slot [[call]] (enfin, les fonctions liées n'en ont pas directement, mais elles procurent une fonction vers le bas).

Cela peut sembler faux car les constructeurs créés avec la syntaxe de classe jettent si vous essayez de les appeler au lieu de les construire. Cependant, ils peuvent être appelés - c'est juste que leur [[call]] slot est défini comme une fonction qui lance! Oy.

Nous pouvons le prouver en convertissant notre première fonction en son image miroir.

// Demonstration only, this function is useless:

const isCallable = fn => {
  try {
    new Proxy(fn, { apply: () => undefined })();
    return true;
  } catch (err) {
    return false;
  }
};

isCallable(() => {});                      // true
isCallable(function() {});                 // true
isCallable(class {});                      // ... true!

Une telle fonction n'est pas utile, mais je voulais montrer ces résultats pour mettre en évidence la nature du problème. La raison pour laquelle nous ne pouvons pas facilement vérifier si une fonction est "nouvelle uniquement" est que la réponse n'est pas modélisée en termes d '"absence d'appel", la façon dont "jamais nouveau" est modélisée en termes d' "absence de construction". Ce que nous voulons savoir est enterré dans une méthode que nous ne pouvons observer que par son évaluation, donc tout ce que nous pouvons faire est d'utiliser des contrôles heuristiques comme proxy pour ce que nous voulons vraiment savoir.

Options heuristiques

Nous pouvons commencer par préciser les cas ambigus. Toute fonction qui n'est pas constructible peut être appelée sans ambiguïté dans les deux sens: si typeof fn === 'function' Mais isConstructable(fn) === false, nous avons un fonction d'appel uniquement telle qu'une flèche, un générateur ou une méthode.

Ainsi, les quatre cas d'intérêt sont class {} Et function() {} plus les formes liées des deux. Tout ce que nous pouvons dire n'est appelable. Notez qu'aucune des réponses actuelles ne mentionne de fonctions liées, mais celles-ci introduisent des problèmes significatifs à toute vérification heuristique.

Comme le souligne balupton, la présence ou l'absence d'un descripteur de propriété pour la propriété "appelant" peut agir comme un indicateur de la façon dont une fonction a été créée. Un objet exotique de fonction liée n'aura pas cette propriété propre même si la fonction qu'il encapsule en possède. La propriété existera via l'héritage de Function.prototype, Mais cela est également vrai pour les constructeurs de classes.

De même, toString pour un BFEO commencera normalement 'function' même si la fonction liée a été créée avec class. Maintenant, une heuristique pour détecter les BFEO eux-mêmes serait de voir si leur nom commence "lié", mais malheureusement c'est une impasse; il ne nous dit toujours rien sur ce que était lié - c'est opaque pour nous.

Cependant, si toString renvoie la `` classe '' (ce qui ne sera pas le cas, par exemple, pour les constructeurs DOM), c'est un signal assez solide qu'il n'est pas appelable.

Le mieux que nous puissions faire est alors quelque chose comme ceci:

const isDefinitelyCallable = fn =>
  typeof fn === 'function' &&
  !isConstructable(fn);

isDefinitelyCallable(class {});                      // false
isDefinitelyCallable(class {}.bind());               // false
isDefinitelyCallable(function() {});                 // false <-- callable
isDefinitelyCallable(function() {}.bind());          // false <-- callable
isDefinitelyCallable(() => {});                      // true
isDefinitelyCallable((() => {}).bind());             // true
isDefinitelyCallable(async () => {});                // true
isDefinitelyCallable(async function() {});           // true
isDefinitelyCallable(function * () {});              // true
isDefinitelyCallable({ foo() {} }.foo);              // true
isDefinitelyCallable(URL);                           // false

const isProbablyNotCallable = fn =>
  typeof fn !== 'function' ||
  fn.toString().startsWith('class') ||
  Boolean(
    fn.prototype &&
    !Object.getOwnPropertyDescriptor(fn, 'prototype').writable // or your fave
  );

isProbablyNotCallable(class {});                      // true
isProbablyNotCallable(class {}.bind());               // false <-- not callable
isProbablyNotCallable(function() {});                 // false
isProbablyNotCallable(function() {}.bind());          // false
isProbablyNotCallable(() => {});                      // false
isProbablyNotCallable((() => {}).bind());             // false
isProbablyNotCallable(async () => {});                // false
isProbablyNotCallable(async function() {});           // false
isProbablyNotCallable(function * () {});              // false
isProbablyNotCallable({ foo() {} }.foo);              // false
isProbablyNotCallable(URL);                           // true

Les cas avec des flèches indiquent où nous obtenons des réponses que nous n'aimons pas particulièrement.

Dans la fonction isProbablyNotCallable, la dernière partie de la condition peut être remplacée par d'autres vérifications d'autres réponses; J'ai choisi Miguel Mota ici, car il se trouve que cela fonctionne aussi avec (la plupart?) Des constructeurs DOM, même ceux définis avant l'introduction des classes ES. Mais cela n'a pas vraiment d'importance - chaque contrôle possible a un inconvénient et il n'y a pas de combo magique.


Ce qui précède décrit à ma connaissance ce qui est et n'est pas possible dans les ES contemporains. Il ne répond pas aux besoins spécifiques à ES5 et versions antérieures, bien qu'en réalité ES5 et versions antérieures, la réponse aux deux questions soit toujours "vraie" pour n'importe quelle fonction.

L'avenir

Il existe une proposition pour un test natif qui rendrait le slot [[FunctionKind]] observable dans la mesure où il révélerait si une fonction a été créée avec class:

https://github.com/caitp/TC39-Proposals/blob/master/tc39-reflect-isconstructor-iscallable.md

Si cette proposition ou quelque chose comme ça avance, nous gagnerions un moyen de résoudre ce problème concrètement en ce qui concerne class au moins.

* Ignorer le cas de l'annexe B [[IsHTMLDDA]].

9
Semicolon

En regardant le code compilé généré par Babel , je pense qu'il n'y a aucun moyen de savoir si une fonction est utilisée comme classe. À l'époque, JavaScript n'avait pas de classes et chaque constructeur n'était qu'une fonction. Le mot-clé de classe JavaScript d'aujourd'hui n'introduit pas un nouveau concept de "classes", c'est plutôt un sucre de syntaxe.

Code ES6:

// ES6
class A{}

ES5 généré par Babel :

// ES5
"use strict";

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var A = function A() {
    _classCallCheck(this, A);
};

Bien sûr, si vous êtes dans les conventions de codage, vous pouvez analyser la fonction (la classe) et vérifier si son nom commence par une majuscule.

function isClass(fn) {
    return typeof fn === 'function' && /^(?:class\s+|function\s+(?:_class|_default|[A-Z]))/.test(fn);
}

ÉDITER:

Les navigateurs qui prennent déjà en charge le mot clé class peuvent l'utiliser lors de l'analyse. Sinon, vous êtes coincé avec la lettre majuscule un.

ÉDITER:

Comme l'a souligné balupton, Babel génère function _class() {} pour les classes anonymes. Regex amélioré basé sur cela.

ÉDITER:

Ajout de _default Au regex, pour détecter des classes comme export default class {}

Avertissement

BabelJS est en cours de développement et il n'y a aucune garantie qu'ils ne changeront pas les noms de fonction par défaut dans ces cas. Vraiment, vous ne devriez pas vous fier à cela.

2
Tamas Hegedus

Vous pouvez utiliser new.target pour déterminer si sa fonction instanciée par la classe ES6 ou le constructeur de fonction

class Person1 {
  constructor(name) {
    this.name = name;
    console.log(new.target) // => // => [Class: Person1]
  }
}

function Person2(){
  this.name='cc'
  console.log(new.target) // => [Function: Person2]
}
1
vinay0079

si j'ai bien compris ES6 en utilisant class a le même effet que si vous tapiez

var Foo = function(){}
var Bar = function(){
 Foo.call(this);
}
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.constructor = Bar;

erreur de syntaxe lors de la saisie de MyClass() sans keyword new est juste pour éviter de polluer l'espace global avec des variables destinées à être utilisées par l'objet.

var MyClass = function(){this.$ = "my private dollar"; return this;}

si tu as

// $ === jquery
var myObject = new MyClass();
// $ === still jquery
// myObject === global object

mais si tu le fais

var myObject = MyClass();
// $ === "My private dollar"

parce que this dans le constructeur appelé comme fonction fait référence à un objet global, mais lorsqu'il est appelé avec le mot clé new Javascript crée d'abord un nouvel objet vide, puis appelle le constructeur dessus.

0
webduvet

Bien qu'il ne soit pas directement lié, mais si la classe, le constructeur ou la fonction est généré par vous et que vous souhaitez savoir si vous devez appeler la fonction ou instancier un objet à l'aide d'un nouveau mot clé, vous pouvez le faire en ajoutant un indicateur personnalisé dans le prototype de le constructeur ou la classe. Vous pouvez certainement distinguer une classe d'une fonction en utilisant les méthodes mentionnées dans d'autres réponses (telles que toString). Cependant, si votre code est transpilé en utilisant babel, ce serait certainement un problème.

Pour le rendre plus simple, vous pouvez essayer le code suivant -

class Foo{
  constructor(){
    this.someProp = 'Value';
  }
}
Foo.prototype.isClass = true;

ou si vous utilisez la fonction constructeur -

function Foo(){
  this.someProp = 'Value';
}
Foo.prototype.isClass = true;

et vous pouvez vérifier s'il s'agit d'une classe ou non en vérifiant la propriété du prototype.

if(Foo.prototype.isClass){
  //It's a class
}

Cette méthode ne fonctionnera évidemment pas si la classe ou la fonction n'est pas créée par vous. React.js utilise cette méthode pour vérifier si le React Component est un composant de classe ou un composant de fonction. Cette réponse est tirée de Dan Abramov article de blog

0
noob