toString: Großartig und schrecklich

Bild


Die toString- Funktion in JavaScript ist wahrscheinlich die "impliziteste", die sowohl unter js-Entwicklern selbst als auch unter externen Beobachtern diskutiert wird. Sie ist die Ursache für zahlreiche Witze und Meme über viele verdächtige arithmetische Operationen, Transformationen, die in ein Stupor [Objekt Objekt] eingehen. Es wird vielleicht nur zugestanden, um bei der Arbeit mit float64 zu überraschen.


Interessante Fälle, die ich beobachten, nutzen oder überwinden musste, motivierten mich, eine echte Nachbesprechung zu schreiben. Wir werden über die Sprachspezifikation galoppieren und die Beispiele verwenden, um die nicht offensichtlichen Merkmale von toString zu analysieren.


Wenn Sie nützliche und ausreichende Anleitungen erwarten, ist dieses , dieses und jenes Material besser für Sie geeignet. Wenn Ihre Neugier immer noch über Pragmatismus herrscht, dann bitte unter Katze.


Alles was Sie wissen müssen


Die Funktion toString ist eine Eigenschaft des Objektprototypobjekts , in einfachen Worten seine Methode. Es wird für die Zeichenfolgenkonvertierung eines Objekts verwendet und sollte auf gute Weise einen primitiven Wert zurückgeben. Die Prototypobjekte haben auch ihre Implementierungen: Funktion, Array, Zeichenfolge, Boolescher Wert, Zahl, Symbol, Datum, RegExp, Fehler . Wenn Sie Ihr Prototypobjekt (Klasse) implementieren, ist toString eine gute Form dafür.


JavaScript ist eine Sprache mit einem schwachen Typsystem: Dies bedeutet, dass wir verschiedene Typen mischen können und viele Operationen implizit ausführen. Bei Konvertierungen wird toString mit valueOf gepaart, um das Objekt auf das für die Operation erforderliche Grundelement zu reduzieren. Beispielsweise wird der Additionsoperator zur Verkettung, wenn sich unter den Operatoren mindestens eine Zeile befindet. Einige Standardfunktionen der Sprache vor ihrer Arbeit führen zu einem Argument für die Zeichenfolge: parseInt, decodeURI, JSON.parse, btoa usw.


Über implizites Casting wurde viel gesagt und verspottet. Wir werden Implementierungen von toString von Prototypenobjekten in Schlüsselsprache in Betracht ziehen.


Object.prototype.toString


Wenn wir uns dem entsprechenden Abschnitt der Spezifikation zuwenden, stellen wir fest, dass die Hauptaufgabe des Standard- toString darin besteht, das sogenannte Tag dazu zu bringen, sich mit der resultierenden Zeichenfolge zu verketten:


"[object " + tag + "]" 

Dafür:


  1. Ein Aufruf des internen toStringTag- Symbols (oder der Pseudo-Eigenschaft [[Class]] in der alten Edition) erfolgt: Es sind viele Prototypobjekte integriert ( Map, Math, JSON und andere).
  2. Wenn eine Zeichenfolge fehlt oder nicht, werden eine Reihe anderer interner Pseudo-Eigenschaften und -Methoden aufgelistet, die den Typ des Objekts signalisieren: [[Aufruf]] für Funktion , [[DateValue]] für Datum usw.
  3. Nun, wenn überhaupt nichts, dann ist das Tag "Objekt" .

Diejenigen, die von der Reflexion betroffen sind , werden sofort die Möglichkeit bemerken, den Typ eines Objekts mit einer einfachen Operation zu erhalten (nicht von der Spezifikation empfohlen, aber möglich):


 const getObjT = obj => Object.prototype.toString.call(obj).match(/\[object\s(\w+)]/)[1]; 

Die Besonderheit der Standardeinstellung für String ist, dass sie mit jedem dieser Werte funktioniert. Wenn es sich um ein Grundelement handelt, wird es in das Objekt umgewandelt ( null und undefiniert werden separat geprüft). Kein TypeError :


 [Infinity, null, x => 1, new Date, function*(){}].map(getObjT); > ["Number", "Null", "Function", "Date", "GeneratorFunction"] 

Wie kann das nützlich sein? Zum Beispiel bei der Entwicklung von Tools für die dynamische Code-Analyse. Mit einem spontanen Pool von Variablen, die während der Arbeit der Anwendung verwendet werden, können Sie zur Laufzeit nützliche homogene Statistiken erfassen.


Dieser Ansatz hat einen Hauptnachteil: Benutzertypen. Es ist nicht schwer zu erraten, dass wir für ihre Instanzen nur "Objekt" bekommen .


Benutzerdefiniertes Symbol.toStringTag und Funktionsname


OOP in JavaScript basiert auf Prototypen und nicht auf Klassen (wie in Java), und wir haben keine vorgefertigte getClass () -Methode. Eine explizite Definition des toStringTag- Zeichens für einen Benutzertyp hilft bei der Lösung des Problems:


 class Cat { get [Symbol.toStringTag]() { return 'Cat'; } } 

oder im Prototypenstil:


 function Dog(){} Dog.prototype[Symbol.toStringTag] = 'Dog'; 

Es gibt eine alternative Lösung über die schreibgeschützte Eigenschaft Function.name , die noch nicht Teil der Spezifikation ist, aber von den meisten Browsern unterstützt wird. Jede Instanz des Prototypobjekts / der Prototypklasse verfügt über eine Verknüpfung zu der Konstruktorfunktion, mit der sie erstellt wurde. So können wir den Namen des Typs herausfinden:


 class Cat {} (new Cat).constructor.name < 'Cat' 

oder im Prototypenstil:


 function Dog() {} (new Dog).constructor.name < 'Dog' 

Natürlich funktioniert diese Lösung nicht für Objekte, die mit einer anonymen Funktion ( "anonym" ) oder Object.create (null) erstellt wurden , oder für Grundelemente ohne Wrapper-Objekt ( null, undefiniert ).


Für eine zuverlässige Manipulation von Variablentypen lohnt es sich daher, bekannte Techniken zu kombinieren, die hauptsächlich auf der jeweiligen Aufgabe basieren. In den allermeisten Fällen reichen Typ und Instanz aus.


Function.prototype.toString


Wir waren ein wenig abgelenkt, aber als Ergebnis kamen wir zu Funktionen, die ihren eigenen interessanten Charakter haben . Schauen Sie sich zunächst den folgenden Code an:


 (function() { console.log('(' + arguments.callee.toString() + ')()'); })() 

Viele vermuteten wahrscheinlich, dass dies ein Beispiel für Quine ist . Wenn Sie ein Skript mit solchen Inhalten in den Hauptteil der Seite laden, wird eine genaue Kopie des Quellcodes in der Konsole angezeigt. Dies ist auf den Aufruf von String aus der Funktion argument.callee zurückzuführen .


Die verwendete Implementierung des Prototypobjekts toString of the Function gibt eine Zeichenfolgendarstellung des Quellcodes der Funktion zurück, wobei die in der Definition verwendete Syntax beibehalten wird : FunctionDeclaration, FunctionExpression, ClassDeclaration, ArrowFunction usw.


Zum Beispiel haben wir eine Pfeilfunktion:


 const bind = (f, ctx) => function() { return f.apply(ctx, arguments); } 

Wenn Sie bind.toString () aufrufen , erhalten Sie eine Zeichenfolgendarstellung von ArrowFunction :


 "(f, ctx) => function() { return f.apply(ctx, arguments); }" 

Der Aufruf von toString von einer umschlossenen Funktion ist bereits eine Zeichenfolgendarstellung von FunctionExpression :


 "function() { return f.apply(ctx, arguments); }" 

Dieses Bindungsbeispiel ist kein Zufall, da wir eine vorgefertigte Lösung mit der Kontextbindung Function.prototype.bind haben. In Bezug auf native gebundene Funktionen gibt es eine Funktion von Function.prototype.toString, die mit ihnen arbeitet. Abhängig von der Implementierung kann eine Darstellung sowohl der umschlossenen Funktion selbst als auch der Zielfunktion erhalten werden. V8 und SpiderMonkey neueste Versionen von Chrome und ff:


 function getx() { return this.x; } getx.bind({ x: 1 }).toString() < "function () { [native code] }" 

Daher ist bei nativ dekorierten Merkmalen Vorsicht geboten.


Üben Sie mit f.toString


Es gibt viele Optionen für die Verwendung des betreffenden toString , die jedoch nur als Metaprogrammierungswerkzeug oder Debugging dringend erforderlich sind. Eine typische Anwendung, die in der Geschäftslogik ähnlich ist, führt früher oder später zu einem nicht unterstützten defekten Trog.


Das Einfachste, was mir in den Sinn kommt, ist die Bestimmung der Länge der Funktion :


 f.toString().replace(/\s+/g, ' ').length 

Die Position und die Anzahl der Leerzeichen des toString- Ergebnisses ergibt sich aus der Spezifikation für den Kauf einer bestimmten Implementierung. Aus Gründen der Sauberkeit entfernen wir daher zuerst den Überschuss, was zu einer allgemeinen Ansicht führt. Übrigens hatte die Funktion in älteren Versionen der Gecko-Engine einen speziellen Einrückungsparameter , der beim Formatieren von Einrückungen hilft.


Die Definition von Funktionsparameternamen fällt sofort ein, was sich zum Nachdenken als nützlich erweisen kann:


 f.toString().match(/^function(?:\s+\w+)?\s*\(([^\)]+)/m)[1].split(/\s*,\s*/) 

Diese Knielösung eignet sich für die Syntax FunctionDeclaration und FunctionExpression . Wenn Sie einen detaillierteren und genaueren benötigen, empfehle ich Ihnen, nach Beispielen für den Quellcode Ihres bevorzugten Frameworks zu suchen, das wahrscheinlich eine Art Abhängigkeitsinjektion unter der Haube hat, basierend auf den Namen der deklarierten Parameter.


Eine gefährliche und interessante Option zum Überschreiben einer Funktion durch Auswertung :


 const sum = (a, b) => a + b; const prod = eval(sum.toString().replace(/\+(?=\s*(?:a|b))/gm, '*')); sum(5, 10) < 15 prod(5, 10) < 50 

Da wir die Struktur der ursprünglichen Funktion kennen, haben wir eine neue erstellt, indem wir den in seinem Körper verwendeten Additionsoperator durch Argumente mit Multiplikation ersetzt haben. Im Fall von Software-generiertem Code oder dem Fehlen einer Funktionserweiterungsschnittstelle kann dies magisch nützlich sein. Wenn Sie beispielsweise ein mathematisches Modell untersuchen, eine geeignete Funktion auswählen und mit Operatoren und Koeffizienten spielen.


Eine praktischere Anwendung ist das Zusammenstellen und Verteilen von Vorlagen . Viele Template-Engine-Implementierungen kompilieren den Quellcode einer Vorlage und stellen eine Datenfunktion bereit, die bereits den endgültigen HTML-Code (oder einen anderen) bildet. Das Folgende ist ein Beispiel für die Funktion _.template :


 const helloJst = "Hello, <%= user %>" _.template(helloJst)({ user: 'admin' }) < "Hello, admin" 

Was aber, wenn das Kompilieren der Vorlage Hardwareressourcen erfordert oder der Client sehr dünn ist? In diesem Fall können wir die Vorlage auf der Serverseite kompilieren und den Clients nicht den Vorlagentext, sondern eine Zeichenfolgendarstellung der fertigen Funktion geben. Darüber hinaus müssen Sie die Vorlagenbibliothek nicht auf den Client laden.


 const helloStr = _.template(helloJst).toString() helloStr < "function(obj) { obj || (obj = {}); var __t, __p = ''; with (obj) { __p += 'Hello, ' + ((__t = ( user )) == null ? '' : __t); } return __p }" 

Jetzt müssen wir diesen Code vor der Verwendung auf dem Client ausführen. Bei der Kompilierung gab es aufgrund der FunctionExpression- Syntax keinen SyntaxError :


 const helloFn = eval(helloStr.replace(/^function\(obj\)/, 'obj=>')); 

oder so:


 const helloFn = eval(`const f = ${helloStr};f`); 

Oder wie du mehr magst. Auf jeden Fall:


 helloFn({ user: 'admin' }) < "Hello, admin" 

Dies ist möglicherweise nicht die beste Vorgehensweise, um Vorlagen auf der Serverseite zu kompilieren und weiter an Clients zu verteilen. Nur ein Beispiel mit einer Reihe von Function.prototype.toString und eval .


Schließlich die alte Aufgabe , einen Funktionsnamen (bevor die Eigenschaft Function.name angezeigt wird) über toString zu definieren :


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

Dies funktioniert natürlich gut mit der FunctionDeclaration- Syntax. Eine intelligentere Lösung erfordert gerissene reguläre Ausdrücke oder Mustervergleiche.


Das Internet ist voll von interessanten Lösungen, die auf Function.prototype.toString basieren. Fragen Sie einfach. Teilen Sie Ihre Erfahrungen in den Kommentaren: sehr interessant.


Array.prototype.toString


Die Implementierung des toString eines Array- Prototypobjekts ist generisch und kann für jedes Objekt aufgerufen werden. Wenn das Objekt über eine Join- Methode verfügt, ist das Ergebnis von toString sein Aufruf, andernfalls Object.prototype.toString .


Array verfügt logischerweise über eine Join-Methode , die die Zeichenfolgendarstellung aller seiner Elemente über das als Parameter übergebene Trennzeichen verkettet (der Standardwert ist ein Komma).


Angenommen, wir müssen eine Funktion schreiben, die eine Liste ihrer Argumente serialisiert. Wenn alle Parameter Grundelemente sind, können wir in vielen Fällen auf JSON.stringify verzichten :


 function seria() { return Array.from(arguments).toString(); } 

oder so:


 const seria = (...a) => a.toString(); 

Denken Sie daran, dass die Zeichenfolge '10' und die Nummer 10 gleich serialisiert werden. Bei dem Problem des kürzesten Memoizers in einer Phase wurde diese Lösung verwendet.


Die native Verknüpfung von Array-Elementen durchläuft einen arithmetischen Zyklus von 0 bis Länge und filtert nicht nach fehlenden Elementen ( null und undefiniert ). Stattdessen tritt eine Verkettung mit dem Trennzeichen auf . Dies führt zu Folgendem:


 const ar = new Array(1000); ar.toString() < ",,,...,,," // 1000 times 

Wenn Sie daher aus dem einen oder anderen Grund dem Array ein Element mit einem großen Index hinzufügen (z. B. eine generierte natürliche ID), verbinden Sie sich in keinem Fall und führen Sie dementsprechend nicht ohne vorherige Vorbereitung zu einer Zeichenfolge. Andernfalls kann dies Konsequenzen haben: Ungültige Zeichenfolgenlänge, zu wenig Speicher oder nur ein baumelndes Skript. Verwenden Sie die Funktionen des Objekts Objektwerte und -schlüssel, um nur die eigenen aufgezählten Eigenschaften des Objekts zu durchlaufen:


 const k = []; k[2**10] = 1; k[2**20] = 2; k[2**30] = 3; Object.values(k).toString() < "1,2,3" Object.keys(k).toString() < "1024,1048576,1073741824" 

Es ist jedoch viel besser, eine solche Behandlung des Arrays zu vermeiden: Höchstwahrscheinlich würde ein einfaches Schlüsselwertobjekt als Speicher für Sie geeignet sein.


Die gleiche Gefahr besteht übrigens bei der Serialisierung über JSON.stringify . Nur schwerwiegender, da leere und nicht unterstützte Elemente bereits als "null" dargestellt werden :


 const ar = new Array(1000); JSON.stringify(ar); < "[null,null,null,...,null,null,null]" // 1000 times 

Abschließend möchte ich Sie daran erinnern, dass Sie Ihre Join- Methode für den Benutzertyp definieren und Array.prototype.toString.call als alternative Umwandlung zum String aufrufen können , aber ich bezweifle, dass dies praktisch sinnvoll ist.


Number.prototype.toString und parseInt


Eine meiner Lieblingsaufgaben für js-Tests ist Was gibt den nächsten parseInt- Aufruf zurück?


 parseInt(10**30, 2) 

Das erste, was parseInt tut, ist implizit ein Argument in einen String umzuwandeln , indem die abstrakte Funktion ToString aufgerufen wird , die je nach Argumenttyp den gewünschten Umwandlungszweig ausführt. Für die Typennummer wird Folgendes ausgeführt:


  1. Wenn der Wert NaN, 0 oder Infinity ist , geben Sie die entsprechende Zeichenfolge zurück.
  2. Andernfalls gibt der Algorithmus den für den Menschen bequemsten Datensatz der Zahl zurück: in Dezimal- oder Exponentialform.

Ich werde den Algorithmus zur Bestimmung der bevorzugten Form hier nicht duplizieren, sondern nur Folgendes beachten: Wenn die Anzahl der Stellen in einer Dezimalschreibweise 21 überschreitet, wird eine Exponentialform ausgewählt. Und das bedeutet, dass parseInt in unserem Fall nicht mit "100 ... 000" funktioniert, sondern mit "1e30". Daher wird die Antwort überhaupt nicht erwartet 2 ^ 30. Wer kennt die Natur dieser magischen Nummer 21 - schreiben Sie!


Als nächstes untersucht parseInt die Basis des verwendeten Radix- Zahlensystems (standardmäßig 10, wir haben 2) und überprüft die Zeichen der empfangenen Zeichenfolge auf Kompatibilität damit. Nachdem er 'e' getroffen hat, schneidet er den gesamten Schwanz ab und lässt nur "1" übrig. Das Ergebnis ist eine Ganzzahl, die durch Konvertieren vom System mit der Radixbasis in eine Dezimalzahl erhalten wird - in unserem Fall ist es 1.


Umgekehrte Vorgehensweise:


 (2**30).toString(2) 

Hier wird die Funktion toString vom Number- Prototypobjekt aufgerufen, das denselben Algorithmus verwendet, um die Zahl in eine Zeichenfolge umzuwandeln. Es hat auch den optionalen Radix- Parameter. Nur es wird ein RangeError für einen ungültigen Wert ausgelöst (es muss eine Ganzzahl von 2 bis einschließlich 36 sein), während parseInt NaN zurückgibt.


Beachten Sie die Obergrenze des Zahlensystems, wenn Sie eine exotische Hash-Funktion implementieren möchten: Dieser toString funktioniert möglicherweise nicht für Sie.


Die Aufgabe, für einen Moment abzulenken:


 '3113'.split('').map(parseInt) 

Was wird zurückgegeben und wie kann das Problem behoben werden?


Der Aufmerksamkeit beraubt


Wir haben toString keineswegs alle nativen Prototypobjekte untersucht. Zum Teil, weil ich persönlich keine Probleme mit ihnen haben musste und es nicht viel Interessantes an ihnen gibt. Außerdem haben wir die Funktion toLocaleString nicht berührt, da es schön wäre, separat darüber zu sprechen. Wenn ich etwas vergeblich getan habe, das der Aufmerksamkeit beraubt, aus den Augen verloren oder missverstanden wurde - schreiben Sie unbedingt!


Aufruf zur Untätigkeit


Die Beispiele, die ich angeführt habe, sind keineswegs fertige Rezepte - nur Denkanstöße. Außerdem finde ich es sinnlos und ein wenig dumm, dies in technischen Interviews zu diskutieren: Dafür gibt es ewige Themen wie Schließungen, Beitritt, eine Ereignisschleife, Modul- / Fassaden- / Mediatormuster und „natürlich“ Fragen zu [dem verwendeten Framework].


Dieser Artikel hat sich als Durcheinander herausgestellt, und ich hoffe, Sie haben etwas Interessantes für sich gefunden. PS Die JavaScript-Sprache - Erstaunlich!


Bonus


Bei der Vorbereitung dieses Materials für die Veröffentlichung habe ich Google Translate verwendet. Und ganz zufällig habe ich einen unterhaltsamen Effekt entdeckt. Wenn Sie eine Übersetzung vom Russischen ins Englische auswählen, "toString" eingeben und mit der Rücktaste löschen, werden wir Folgendes beobachten:


Bonus


So eine Ironie! Ich glaube, ich bin weit vom ersten entfernt, aber nur für den Fall, dass ich ihnen einen Screenshot mit einem Wiedergabeskript geschickt habe. Es sieht aus wie ein harmloses Selbst-XSS, deshalb teile ich es.

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


All Articles