Art von Alles und Enten Missverständnissen

Bild


Jeder, der wundervolles JavaScript für irgendeinen Zweck verwendet, fragte sich: Warum ist typeof null "Objekt" ? typeof von einer Funktion gibt "function" zurück , aber von Array - "object" ? und wo getClass in deinen gepriesenen Klassen? Und obwohl zum größten Teil die Spezifikation und die historischen Fakten leicht und natürlich zu beantworten sind, möchte ich ein wenig ... mehr für mich selbst ziehen.


Wenn, Leser, Ihr Typ und Ihre Instanz für Ihre Aufgaben nicht ausreichen und Sie einige Besonderheiten anstelle von "Objekten" wünschen, kann dies später hilfreich sein. Oh ja, über die Enten: Sie werden es auch sein, nur ein bisschen falsch.


Kurzer Hintergrund


Das zuverlässige Abrufen des Variablentyps in JavaScript war schon immer keine triviale Aufgabe, schon gar nicht für Anfänger. In den meisten Fällen ist es natürlich nicht erforderlich, nur:


if (typeof value === 'object' && value !== null) { // awesome code around the object } 

und jetzt fangen Sie nicht ab Cannot read property of null - ein lokales Analogon von NPE. Ist das bekannt?


Und dann haben wir immer häufiger Funktionen als Konstruktoren verwendet, und manchmal ist es nützlich, den Typ eines auf diese Weise erstellten Objekts zu untersuchen. Die Verwendung von typeof aus der Instanz funktioniert jedoch nicht, da das "Objekt" korrekt abgerufen wird.


Dann war es noch normal, ein Prototyp-OOP-Modell in JavaScript zu verwenden. Wir haben ein Objekt, und über den Link zu seinem Prototyp können wir die Konstruktoreigenschaft finden, die auf die Funktion verweist, mit der das Objekt erstellt wurde. Und dann ein bisschen Magie mit toString aus der Funktion und den regulären Ausdrücken, und hier ist das Ergebnis:


 f.toString().match(/function\s+(\w+)(?=\s*\()/m)[1] 

Manchmal stellten sie solche Fragen bei Interviews, aber warum?


Ja, wir könnten einfach eine Zeichenfolgendarstellung des Typs als spezielle Eigenschaft speichern und sie über die Prototypenkette vom Objekt abrufen:


 function Type() {}; Type.prototype.typeName = 'Type'; var o = new Type; o.typeName; < "Type" 

Nur zweimal müssen Sie "Typ" schreiben: in die Funktionsdeklaration und in die Eigenschaft.


Für die eingebauten Objekte (wie Array oder Date ) hatten wir eine geheime Eigenschaft [[Class]] , die vom Standardobjekt aus anString angehängt werden konnte :


 Object.prototype.toString.call(new Array); < "[object Array]" 

Jetzt haben wir Klassen und benutzerdefinierte Typen sind endlich in der Sprache festgelegt: Dies ist kein LiveScript für Sie; Wir schreiben unterstützten Code in großen Mengen!


Etwa zur gleichen Zeit erschienen Symbol.toStringTag und Function.name , mit denen wir unseren Typ auf eine neue Art und Weise nehmen können.


Bevor wir fortfahren, möchte ich generell darauf hinweisen, dass sich das betrachtete Problem bei StackOverflow zusammen mit der Sprache entwickelt und von Editor zu Editor steigt: vor 9 Jahren , vor 7 Jahren , vor nicht allzu langer Zeit, oder ist dies und das .


Aktueller Stand der Dinge


Zuvor haben wir Symbol.toStringTag und Function.name bereits ausführlich genug untersucht . Kurz gesagt, das interne toStringTag- Zeichen ist eine moderne [[Klasse]] , nur wir können es für unsere Objekte neu definieren. Und die Function.name- Eigenschaft ist in fast allen Browsern mit demselben Typnamen wie im Beispiel legalisiert: Gibt den Namen der Funktion zurück.


Ohne zu zögern können Sie eine solche Funktion definieren:


 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]; } 

  1. WENN die Variable ein Objekt ist, dann:
    1.1. Wenn das Objekt in StringTag überschrieben wird, geben Sie es zurück.
    1.2. WENN die Konstruktorfunktion für das Objekt bekannt ist und die Funktion eine Namenseigenschaft hat, geben Sie diese zurück.
  2. ENDLICH ANDERWEITIG verwenden Sie die toString- Methode des Object-Objekts , die die gesamte polymorphe Arbeit für absolut jede andere Variable für uns erledigt.

Objekt mit toStringTag :


 let kitten = { [Symbol.toStringTag]: 'Kitten' }; getTag(kitten); < "Kitten" 

Klasse mit toStringTag :


 class Cat { get [Symbol.toStringTag]() { return 'Kitten'; } } getTag(new Cat); < "Kitten" 

Verwenden von constructor.name :


 class Dog {} getTag(new Dog); < "Dog" 

→ Weitere Beispiele finden Sie in diesem Repository.


Daher ist es jetzt ziemlich einfach, den Typ einer Variablen in JavaScript zu bestimmen. Mit dieser Funktion können Sie Variablen einheitlich auf Typ prüfen und einen einfachen Schalterausdruck anstelle von Entenprüfungen in polymorphen Funktionen verwenden. Ich mochte den Ansatz, der auf der Eingabe von Enten basiert, nie wirklich. Wenn etwas die Spleiß- Eigenschaft hat, ist es ein Array oder so?


Einige falsche Enten


Das Verständnis des Typs einer Variablen durch das Vorhandensein bestimmter Methoden oder Eigenschaften ist natürlich für jeden von der Situation abhängig. Aber ich werde diesen getTag nehmen und einige Sprachartikel untersuchen.


Klassen?


Meine Lieblingsente in JavaScript sind die Klassen. Die Leute, die angefangen haben, JavaScript mit ES-2015 zu schreiben, ahnen manchmal nicht, was diese Klassen sind. Und die Wahrheit ist:


 class Person { constructor(name) { this.name = name; } hello() { return this.name; } } let user = new Person('John'); user.hello(); < "John" 

Wir haben ein Klassenschlüsselwort , einen Konstruktor, einige Methoden, sogar erweitert . Wir erstellen auch eine Instanz dieser Klasse durch new . Es sieht aus wie eine Klasse im üblichen Sinne - es bedeutet eine Klasse!


Wenn Sie jedoch beginnen, der "Klasse" in Echtzeit neue Methoden hinzuzufügen, die gleichzeitig für bereits erstellte Instanzen sofort verfügbar sind, gehen einige verloren:


 Person.prototype.hello = function() { return `Is not ${this.name}`; } user.hello(); < "Is not John" 

Tu das nicht!


Und nicht einige wissen zuverlässig, dass dies nur syntaktischer Zucker gegenüber dem Prototypmodell ist, da sich konzeptionell nichts an der Sprache geändert hat. Wenn wir getTag von Person aufrufen, erhalten wir "Funktion" , nicht die erfundene "Klasse" , und dies ist es wert, in Erinnerung zu bleiben.


Andere Funktionen


Es gibt verschiedene Möglichkeiten, eine Funktion in JavaScript zu deklarieren: FunctionDeclaration , FunctionExpression und kürzlich ArrowFunction . Wir alle wissen, wann und was wir verwenden müssen: Die Dinge sind ganz anders. Wenn wir außerdem getTag von der Funktion aus aufrufen, die von einer der vorgeschlagenen Optionen deklariert wurde, erhalten wir "Function" .


Tatsächlich gibt es viel mehr Möglichkeiten, eine Funktion in einer Sprache zu definieren. Fügen Sie der Liste mindestens die betrachtete ClassDeclaration hinzu , dann asynchrone Funktionen und Generatoren usw.: AsyncFunctionDeclaration , AsyncFunctionExpression , AsyncArrowFunction , GeneratorDeclaration , GeneratorExpression , ClassExpression , MethodDefinition (die Liste ist nicht vollständig). Und es scheint, dass sie so was sagen? Alles oben Genannte verhält sich wie eine Funktion - was bedeutet, dass getTag auch "Function" zurückgibt . Aber es gibt eine Eigenschaft: Alle Optionen sind sicherlich Funktionen, aber nicht alle direkt - Funktion .


Es gibt integrierte Untertypen von Funktionen :


Der Funktionskonstruktor ist so konzipiert, dass er in Unterklassen unterteilt werden kann.
Es gibt keine syntaktischen Mittel zum Erstellen von Instanzen von Funktionsunterklassen mit Ausnahme der integrierten Unterklassen GeneratorFunction und AsyncFunction.

Wir haben Function und Inside GeneratorFunction und AsyncFunction mit ihren Konstruktoren, die davon "geerbt" wurden. Dies unterstreicht, dass Asinks und Generatoren ihre eigene einzigartige Natur haben. Und als Ergebnis:


 async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } getTag(sleep); < "AsyncFunction" 

Gleichzeitig können wir eine solche Funktion nicht über den neuen Operator instanziieren, und sein Aufruf gibt Promise an uns zurück:


 getTag(sleep(100)); < "Promise" 

Ein Beispiel mit einer Generatorfunktion:


 function* incg(i) { while(1) yield i += 1; } getTag(incg); < "GeneratorFunction" 

Das Aufrufen einer solchen Funktion gibt eine Instanz zurück - das Generator- Objekt:


 let inc = incg(0); getTag(inc); < "Generator" 

Das toStringTag- Symbol ist für Asinks und Generatoren ziemlich neu definiert. Der Typ für jede Funktion zeigt jedoch "Funktion" an .


Inline-Objekte


Wir haben Dinge wie Set , Map , Date oder Error . Wenn Sie getTag auf sie anwenden , wird "Function" zurückgegeben , da dies die Funktionen sind - Konstruktoren iterierbarer Sammlungen, Daten und Fehler. Von den Instanzen erhalten wir jeweils - "Set" , "Map" , "Date" und " Error ".


Aber aufgepasst! Es gibt immer noch Objekte wie JSON oder Math . Wenn Sie sich beeilen, können Sie eine ähnliche Situation annehmen. Aber nein! Das ist völlig anders - eingebaute Einzelobjekte. Sie sind nicht instanziierbar ( is not a constructor ). Eine Art von Aufruf gibt "Objekt" zurück (wer würde es bezweifeln). GetTag wendet sich jedoch an StringTag und erhält "JSON" und "Math" . Dies ist die letzte Beobachtung, die ich teilen möchte.


Gehen wir ohne Fanatismus


Ich habe kürzlich etwas tiefer als gewöhnlich nach dem Typ einer Variablen in JavaScript gefragt, als ich mich entschied, meinen einfachen Objektinspektor für die dynamische Code-Analyse (Herumspielen) zu schreiben. Das Material wird nicht nur in Abnormaler Programmierung veröffentlicht , da Sie es in der Produktion nicht benötigen: Es gibt typeof , instanceof , Array.isArray , isNaN und alles andere, an das Sie sich bei der Durchführung der erforderlichen Überprüfung erinnern sollten. Die meisten Projekte haben TypeScript, Dart oder Flow. Ich liebe JavaScript !

Source: https://habr.com/ru/post/de427253/


All Articles