
Tout le monde utilise un merveilleux JavaScript pour n'importe quel but se demande: pourquoi est-ce que typeof est un "objet" nul ? typeof d'une fonction renvoie "fonction" , mais de Array - "objet" ? et où getClass dans vos cours tant vantés? Et bien que pour l'essentiel les spécifications et les faits historiques répondent facilement et naturellement, je voudrais tracer un peu un trait ... plus pour moi.
Si, lecteur, votre typeof et instanceof ne vous suffisent pas dans vos tâches, et que vous voulez des détails, plutôt que des "objets" , alors cela peut être utile plus tard. Oh oui, à propos des canards: ils le seront aussi, juste un peu mal.
Bref historique
Obtenir de manière fiable le type d'une variable en JavaScript a toujours été une tâche non triviale, certainement pas pour un débutant. Dans la plupart des cas, ce n'est bien sûr pas obligatoire, juste:
if (typeof value === 'object' && value !== null) {
et maintenant vous n'attrapez pas Cannot read property of null
- un analogue local de NPE. Est-ce familier?
Et puis nous avons commencé à utiliser les fonctions plus souvent en tant que constructeurs, et il est parfois utile d'inspecter le type de l'objet créé de cette façon. Mais l'utilisation de typeof à partir de l'instance ne fonctionnera pas, car nous obtenons correctement "l'objet" .
Ensuite, il était toujours normal d'utiliser un prototype de modèle OOP en JavaScript, vous vous en souvenez? Nous avons un objet, et grâce au lien vers son prototype, nous pouvons trouver la propriété constructeur qui pointe vers la fonction avec laquelle l'objet a été créé. Et puis un peu de magie avec toString de la fonction et des expressions régulières, et voici le résultat:
f.toString().match(/function\s+(\w+)(?=\s*\()/m)[1]
Parfois, ils ont posé de telles questions lors des entretiens, mais pourquoi?
Oui, nous pourrions simplement enregistrer une représentation sous forme de chaîne du type en tant que propriété spéciale et l'obtenir à partir de l'objet via la chaîne de prototype:
function Type() {}; Type.prototype.typeName = 'Type'; var o = new Type; o.typeName; < "Type"
Vous n'avez qu'à écrire deux fois "Type" : dans la déclaration de fonction et dans la propriété.
Pour les objets intégrés (comme Array ou Date ), nous avions une propriété secrète [[Class]] , qui pouvait être accrochée via toString à partir de l' objet standard:
Object.prototype.toString.call(new Array); < "[object Array]"
Nous avons maintenant des classes, et les types personnalisés sont finalement corrigés dans le langage: ce n'est pas un LiveScript pour vous; nous écrivons du code pris en charge en grande quantité!
Vers la même époque, Symbol.toStringTag et Function.name sont apparus , avec lesquels nous pouvons prendre notre typeof d'une nouvelle manière.
En général, avant de poursuivre, je tiens à noter que la question à l'examen évolue avec StackOverflow avec le langage et remonte du comité de rédaction au comité de rédaction: il y a 9 ans , 7 ans, il n'y a pas si longtemps, ou ceci et cela .
Situation actuelle
Plus tôt, nous avons déjà examiné suffisamment en détail Symbol.toStringTag et Function.name . En bref, le caractère interne toStringTag est moderne [[Class]] , nous seuls pouvons le redéfinir pour nos objets. Et la propriété Function.name est légalisée dans presque tous les navigateurs du même typeName de l'exemple: renvoie le nom de la fonction.
Sans hésitation, vous pouvez définir une telle fonction:
function getTag(any) { if (typeof any === 'object' && any !== null) { if (typeof any[Symbol.toStringTag] === 'string') { return any[Symbol.toStringTag]; } if (typeof any.constructor === 'function' && typeof any.constructor.name === 'string') { return any.constructor.name; } } return Object.prototype.toString.call(any).match(/\[object\s(\w+)]/)[1]; }
- SI la variable est un objet, alors:
1.1. SI l'objet est substitué àStringTag , puis retournez-le;
1.2. SI la fonction constructeur est connue pour l'objet et que la fonction a une propriété de nom , renvoyez-la; - ENFIN AUTREMENT, utilisez la méthode toString de l' objet Object , qui fera tout le travail polymorphe pour nous pour absolument toute autre variable.
Objet avec toStringTag :
let kitten = { [Symbol.toStringTag]: 'Kitten' }; getTag(kitten); < "Kitten"
Classe avec toStringTag :
class Cat { get [Symbol.toStringTag]() { return 'Kitten'; } } getTag(new Cat); < "Kitten"
Utilisation de constructor.name :
class Dog {} getTag(new Dog); < "Dog"
→ Plus d'exemples peuvent être trouvés dans ce référentiel.
Ainsi, il est assez facile maintenant de déterminer le type de n'importe quelle variable en JavaScript. Cette fonction vous permet de vérifier uniformément le type des variables et d'utiliser une expression de commutateur simple au lieu des vérifications de canard dans les fonctions polymorphes. En général, je n'ai jamais aimé l'approche basée sur le typage du canard, ils disent que si quelque chose a la propriété splice , est-ce un tableau ou quelque chose?
Quelques mauvais canards
La compréhension du type d'une variable par la présence de certaines méthodes ou propriétés est bien sûr l'affaire de tous et dépend de la situation. Mais je vais prendre ce getTag et inspecter quelques trucs de langue.
Des cours?
Mon «canard» préféré en JavaScript est les cours. Les gars qui ont commencé à écrire JavaScript avec ES-2015, ne soupçonnent parfois pas ce que sont ces classes. Et la vérité est:
class Person { constructor(name) { this.name = name; } hello() { return this.name; } } let user = new Person('John'); user.hello(); < "John"
Nous avons un mot - clé de classe , un constructeur, certaines méthodes, voire des extensions . Nous créons également une instance de cette classe via new . Cela ressemble à une classe dans son sens habituel - cela signifie une classe!
Cependant, lorsque vous commencez à ajouter de nouvelles méthodes en temps réel à la "classe" et qu'elles deviennent en même temps immédiatement disponibles pour les instances déjà créées, certaines sont perdues:
Person.prototype.hello = function() { return `Is not ${this.name}`; } user.hello(); < "Is not John"
Ne fais pas ça!
Et certains ne savent pas de manière fiable qu'il ne s'agit que de sucre syntaxique par rapport au modèle prototype, car conceptuellement rien n'a changé dans le langage. Si nous appelons getTag de Person , nous obtenons "Function" , pas la "Class" inventée , et cela mérite d'être rappelé.
Autres fonctions
Il existe plusieurs façons de déclarer une fonction en JavaScript: FunctionDeclaration , FunctionExpression et récemment ArrowFunction . Nous savons tous quand et quoi utiliser: les choses sont très différentes. De plus, si nous appelons getTag à partir de la fonction déclarée par l'une des options proposées, nous obtenons "Function" .
En fait, le langage a beaucoup plus de façons de définir une fonction. Ajoutez à la liste au moins la classe ClassDeclaration , puis les fonctions et générateurs asynchrones, etc.: AsyncFunctionDeclaration , AsyncFunctionExpression , AsyncArrowFunction , GeneratorDeclaration , GeneratorExpression , ClassExpression , MethodDefinition (la liste n'est pas complète). Et il semble qu'ils disent quoi? tout ce qui précède se comporte comme une fonction - ce qui signifie que getTag renverra également "Function" . Mais il y a une caractéristique: toutes les options sont certainement des fonctions, mais pas toutes directement - Fonction .
Il existe des sous-types de fonction intégrés :
Le constructeur Function est conçu pour être sous-classable.
Il n'existe aucun moyen syntaxique pour créer des instances de sous-classes Function, à l'exception des sous-classes GeneratorFunction et AsyncFunction intégrées.
Nous avons Function et Inside GeneratorFunction et AsyncFunction avec leurs constructeurs "hérités" de celui-ci. Cela souligne que les puits et les générateurs ont leur propre nature unique. Et par conséquent:
async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } getTag(sleep); < "AsyncFunction"
Dans le même temps, nous ne pouvons pas instancier une telle fonction via le nouvel opérateur, et son appel nous renvoie Promise :
getTag(sleep(100)); < "Promise"
Un exemple avec une fonction générateur:
function* incg(i) { while(1) yield i += 1; } getTag(incg); < "GeneratorFunction"
L'appel d'une telle fonction renvoie une instance - l'objet Generator :
let inc = incg(0); getTag(inc); < "Generator"
Le symbole toStringTag est assez redéfini pour les asinks et les générateurs. Mais typeof pour n'importe quelle fonction affichera "fonction" .
Objets en ligne
Nous avons des choses comme Set , Map , Date ou Error . Leur appliquer getTag renverra "Function" , car ce sont les fonctions - constructeurs de collections itérables, dates et erreurs. Des instances que nous obtenons respectivement - "Set" , "Map" , "Date" et " Error ".
Mais attention! il y a encore des objets comme JSON ou Math . Si vous vous dépêchez, vous pouvez supposer une situation similaire. Mais non! c'est complètement différent - des objets uniques intégrés. Ils ne sont pas instanciables ( is not a constructor
). Un appel de type renverra "objet" (qui en douterait). Mais getTag se tournera vers toStringTag et obtiendra "JSON" et "Math" . Ceci est la dernière observation que je veux partager.
Allons sans fanatisme
J'ai posé des questions un peu plus profondément que d'habitude sur le type d'une variable en JavaScript récemment lorsque j'ai décidé d'écrire mon simple inspecteur d'objets pour l'analyse de code dynamique (jouer). Le matériel n'est pas seulement publié dans la programmation anormale , car vous n'en avez pas besoin en production: il y a typeof , instanceof , Array.isArray , isNaN et tout ce qui mérite d'être rappelé lors de la vérification nécessaire. La plupart des projets ont TypeScript, Dart ou Flow. J'adore JavaScript !