JavaScript-Metaprogrammierung

Metaprogrammierung ist eine Art der Programmierung, die mit der Erstellung von Programmen verbunden ist, die aufgrund ihrer Arbeit andere Programme generieren, oder von Programmen, die sich während der Ausführung selbst ändern. (Wikipedia)

In einer einfacheren Sprache kann die Metaprogrammierung in JavaScript als Mechanismus betrachtet werden, mit dem Sie das Programm je nach Aktion in Echtzeit analysieren und ändern können. Und höchstwahrscheinlich verwenden Sie sie irgendwie, wenn Sie jeden Tag Skripte schreiben.


JavaScript ist von Natur aus eine sehr leistungsfähige dynamische Sprache und ermöglicht es Ihnen, flexiblen Code gut zu schreiben:


/** *   save-    */ const comment = { authorId: 1, comment: '' }; for (let name in comment) { const pascalCasedName = name.slice(0, 1).toUpperCase() + name.slice(1); comment[`save${pascalCasedName}`] = function() { //   } } comment.saveAuthorId(); //  authorId comment.saveComment(); //  comment 

Ähnlicher Code zum dynamischen Erstellen von Methoden in anderen Sprachen erfordert häufig eine spezielle Syntax oder API. Zum Beispiel ist PHP auch eine dynamische Sprache, die jedoch mehr Aufwand erfordert:


 <?php class Comment { public $authorId; public $comment; public function __construct($authorId, $comment) { $this->authorId = $authorId; $this->comment = $comment; } //       public function __call($methodName, $arguments) { foreach (get_object_vars($this) as $fieldName => $fieldValue) { $saveMethodName = "save" . strtoupper($fieldName[0]) . substr($fieldName, 1); if ($methodName == $saveMethodName) { //   } } } } $comment = new Comment(1, ''); $comment->saveAuthorId(); //  authorId $comment->saveComment(); //  comment 

Zusätzlich zur flexiblen Syntax bieten wir eine Reihe nützlicher Funktionen zum Schreiben von dynamischem Code: Object.create, Object.defineProperty, Function.apply und viele andere.


Betrachten Sie sie genauer.


  1. Codegenerierung
  2. Mit Funktionen arbeiten
  3. Mit Objekten arbeiten
  4. API widerspiegeln
  5. Symbole
  6. Proxy
  7. Fazit

1. Codegenerierung


Das Standardwerkzeug zum dynamischen Ausführen von Code ist die Auswertungsfunktion , mit der Sie Code aus der übergebenen Zeichenfolge ausführen können:


 eval('alert("Hello, world")'); 

Leider hat eval viele Nuancen:


  • Wenn unser Code im strengen Modus geschrieben ist ('use strict'), sind die in eval deklarierten Variablen im aufrufenden eval-Code nicht sichtbar. Gleichzeitig kann der Code selbst in eval immer externe Variablen ändern.
  • Der Code in eval kann sowohl im globalen Kontext (wenn er über window.eval aufgerufen wird) als auch im Kontext der Funktion ausgeführt werden, in der der Aufruf ausgeführt wurde (wenn nur eval, ohne Fenster).
  • Probleme können aufgrund der JS-Minimierung auftreten, wenn Variablennamen durch kürzere ersetzt werden, um die Größe zu verringern. Der Code, der als Zeichenfolge an eval übergeben wird, berührt normalerweise nicht den Minifier. Aus diesem Grund können wir mit alten nicht minimierten Namen auf externe Variablen zugreifen, was zu subtilen Fehlern führt.

Es gibt eine großartige Alternative zur Lösung dieser Probleme - eine neue Funktion .


 const hello = new Function('name', 'alert("Hello, " + name)'); hello('') // alert("Hello, "); 

Im Gegensatz zu eval können wir Parameter immer explizit durch die Argumente einer Funktion übergeben und ihr dynamisch den Kontext geben (über Function.apply oder Function.call ). Darüber hinaus wird die erstellte Funktion immer im globalen Bereich aufgerufen.


In den alten Tagen wurde eval oft verwendet, um Code dynamisch zu ändern JavaScript hatte nur sehr wenige Mechanismen zur Reflexion und es war unmöglich, auf eine Auswertung zu verzichten. Im modernen Sprachstandard ist jedoch eine viel höhere Funktionalität aufgetaucht, und die Bewertung wird jetzt viel seltener verwendet.


2. Arbeiten Sie mit Funktionen


JavaScript bietet uns viele hervorragende Tools für die dynamische Arbeit mit Funktionen, mit denen wir zur Laufzeit verschiedene Informationen über die Funktion abrufen und ändern können:


  • Function.length - Mit dieser Funktion können Sie die Anzahl der Argumente einer Funktion ermitteln:


     const func = function(name, surname) { console.log(`Hello, ${surname} ${name}`) }; console.log(func.length) // 2 

  • Function.apply und Function.call - Mit dieser Option können Sie den Kontext dieser Funktion dynamisch ändern:


     const person = { name: '', introduce: function() { return ` ${this.name}`; } } person.introduce(); //   person.introduce.call({ name: '' }); //   

    Sie unterscheiden sich nur dadurch, dass in Function.apply die Argumente für die Funktion als Array und in Function.call durch Kommas getrennt werden. Diese Funktion wurde zuvor häufig verwendet, um eine Liste von Argumenten als Array an die Funktion zu übergeben. Ein häufiges Beispiel ist die Math.max- Funktion (standardmäßig funktioniert sie nicht mit Arrays):


     Math.max.apply(null, [1, 2, 4, 3]); // 4 

    Mit dem Aufkommen des neuen Spread-Operators können Sie einfach Folgendes schreiben:


     Math.max(...[1, 2, 4, 3]); // 4 

  • Function.bind - Ermöglicht das Erstellen einer Kopie einer Funktion aus einer vorhandenen, jedoch mit einem anderen Kontext:


     const person = { name: '', introduce: function() { return ` ${this.name}`; } } person.introduce(); //   const introduceEgor = person.introduce.bind({ name: '' }); introduceEgor(); //   

  • Function.caller - Ermöglicht das Abrufen der aufrufenden Funktion. Es wird nicht empfohlen, es zu verwenden , da es im Sprachstandard nicht vorhanden ist und nicht im strengen Modus funktioniert. Dies lag an der Tatsache, dass der Aufruf von Function.caller möglicherweise zu falschen Ergebnissen führt, wenn verschiedene JavaScript-Engines die in der Sprachspezifikation beschriebene Tail-Call- Optimierung implementieren. Anwendungsbeispiel:


     const a = function() { console.log(a.caller == b); } const b = function() { a(); } b(); // true 

  • Function.toString - Gibt eine Zeichenfolgendarstellung der Funktion zurück. Dies ist eine sehr leistungsstarke Funktion, mit der Sie sowohl den Inhalt einer Funktion als auch ihre Argumente untersuchen können:


     const getFullName = (name, surname, middlename) => { console.log(`${surname} ${name} ${middlename}`); } getFullName.toString() /* * "(name, surname, middlename) => { * console.log(`${surname} ${name} ${middlename}`); * }" */ 

    Nachdem wir eine Zeichenfolgendarstellung einer Funktion erhalten haben, können wir sie analysieren und analysieren. Dies kann beispielsweise verwendet werden, um die Namen von Funktionsargumenten abzurufen und je nach Namen automatisch den gewünschten Parameter zu ersetzen. Im Allgemeinen gibt es zwei Möglichkeiten zum Parsen:


    • Wenn Sie eine Reihe von Stammgästen analysieren, erhalten Sie ein akzeptables Maß an Zuverlässigkeit (funktioniert möglicherweise nicht, wenn wir nicht alle möglichen Arten von Funktionseinträgen abdecken).
    • Wir erhalten die Zeichenfolgendarstellung der Funktion und fügen sie in den fertigen JavaScript-Parser (z. B. Esprima oder Eichel ) ein. Anschließend arbeiten wir mit dem strukturierten AST. AST-Parsing-Beispiel über Esprima. Ich kann auch einen guten Bericht über Parser von Alexei Okhrimenko empfehlen.


Einfache Beispiele mit regulärer Funktionsanalyse:


Eine Liste der Funktionsargumente abrufen
 /** *    . * @param fn  */ const getFunctionParams = fn => { const COMMENTS = /(\/\/.*$)|(\/\*[\s\S]*?\*\/)|(\s*=[^,\)]*(('(?:\\'|[^'\r\n])*')|("(?:\\"|[^"\r\n])*"))|(\s*=[^,\)]*))/gm; const DEFAULT_PARAMS = /=[^,]+/gm; const FAT_ARROW = /=>.*$/gm; const ARGUMENT_NAMES = /([^\s,]+)/g; const formattedFn = fn .toString() .replace(COMMENTS, "") .replace(FAT_ARROW, "") .replace(DEFAULT_PARAMS, ""); const params = formattedFn .slice(formattedFn.indexOf("(") + 1, formattedFn.indexOf(")")) .match(ARGUMENT_NAMES); return params || []; }; const getFullName = (name, surname, middlename) => { console.log(surname + ' ' + name + ' ' + middlename); }; console.log(getFunctionParams(getFullName)); // ["name", "surname", "middlename"] 


Körperfunktion bekommen
 /** *     . * @param fn  */ const getFunctionBody = fn => { const restoreIndent = body => { const lines = body.split("\n"); const bodyLine = lines.find(line => line.trim() !== ""); let indent = typeof bodyLine !== "undefined" ? (/[ \t]*/.exec(bodyLine) || [])[0] : ""; indent = indent || ""; return lines.map(line => line.replace(indent, "")).join("\n"); }; const fnStr = fn.toString(); const rawBody = fnStr.substring( fnStr.indexOf("{") + 1, fnStr.lastIndexOf("}") ); const indentedBody = restoreIndent(rawBody); const trimmedBody = indentedBody.replace(/^\s+|\s+$/g, ""); return trimmedBody; }; //       getFullName const getFullName = (name, surname, middlename) => { console.log(surname + ' ' + name + ' ' + middlename); }; console.log(getFunctionBody(getFullName)); 


Es ist wichtig zu beachten, dass bei Verwendung des Minifizierers sowohl der Code selbst innerhalb der analysierten Funktion als auch seine Argumente optimiert werden können und sich daher ändern können.


3. Arbeiten Sie mit Objekten


JavaScript verfügt über ein globales Objektobjekt, das viele Methoden zum dynamischen Arbeiten mit Objekten enthält.


Die meisten dieser Methoden von dort existieren seit langem in der Sprache und sind weit verbreitet.


Objekteigenschaften


  • Object.assign - um die Eigenschaften eines oder mehrerer Objekte bequem in das durch den ersten Parameter angegebene Objekt zu kopieren:


     Object.assign({}, { a: 1 }, { b: 2 }, { c: 3 }) // {a: 1, b: 2, c: 3} 

  • Object.keys und Object.values - gibt entweder eine Liste von Schlüsseln oder eine Liste von Objektwerten zurück:


     const obj = { a: 1, b: 2, c: 3 }; console.log(Object.keys(obj)); // ["a", "b", "c"] console.log(Object.values(obj)); // [1, 2, 3] 

  • Object.entries - gibt eine Liste seiner Eigenschaften im Format [[key1, value1], [key2, value2]] zurück :


     const obj = { a: 1, b: 2, c: 3 }; console.log(Object.entries(obj)); // [["a", 1], ["b", 2], ["c", 3]] 

  • Object.prototype.hasOwnProperty - Überprüft, ob eine Eigenschaft in einem Objekt enthalten ist (nicht in seiner Prototypenkette):


     const obj = { a: 1 }; obj.__proto__ = { b: 2 }; console.log(obj.hasOwnProperty('a')); // true console.log(obj.hasOwnProperty('b')) // false 

  • Object.getOwnPropertyNames - Gibt eine Liste seiner eigenen Eigenschaften zurück, einschließlich aufgezählter und nicht aufgezählter:


     const obj = { a: 1, b: 2 }; Object.defineProperty(obj, 'c', { value: 3, enumerable: false }); //    for (let key in obj) { console.log(key); } // "a", "b" console.log(Object.getOwnPropertyNames(obj)); // [ "a", "b", "c" ] 

  • Object.getOwnPropertySymbols - Gibt eine Liste eigener Zeichen (im Objekt und nicht in der Prototypenkette enthalten) zurück:


     const obj = {}; const a = Symbol('a'); obj[a] = 1; console.log(Object.getOwnPropertySymbols(obj)); // [ Symbol(a) ] 

  • Object.prototype.propertyIsEnumerable - Überprüft, ob eine Eigenschaft aufzählbar ist (z. B. in for- in- und for-of-Schleifen verfügbar ist):


     const arr = [ ' ' ]; console.log(arr.propertyIsEnumerable(0)); // true —  ' '   console.log(arr.propertyIsEnumerable('length')); // false —  length    


Objekteigenschaftsbeschreibungen


Mit Deskriptoren können Sie die Eigenschaftsparameter optimieren. Mit ihnen können wir bequem unsere eigenen Interceptors erstellen, während wir eine Eigenschaft lesen / schreiben (Getter und Setter - get / set), Eigenschaften unveränderlich oder nicht aufzählbar machen und eine Reihe anderer Dinge.


  • Object.defineProperty und Object.defineProperties - Erstellt einen oder mehrere Eigenschaftsbeschreibungen. Erstellen Sie Ihren eigenen Deskriptor mit Getter und Setter:


     const obj = { name: '', surname: '' }; Object.defineProperty(obj, 'fullname', { //     fullname get: function() { return `${this.name} ${this.surname}`; }, //     fullname (     delete obj.fullname) set: function(value) { const [name, surname] = value.split(' '); this.name = name; this.surname = surname; }, }); console.log(obj.fullname); //   obj.fullname = ' '; console.log(obj.name); //  console.log(obj.surname); //  

    Im obigen Beispiel hatte die Eigenschaft fullname keinen eigenen Wert, sondern arbeitete dynamisch mit den Eigenschaften name und Nachname. Es ist nicht erforderlich, sowohl einen Getter als auch einen Setter gleichzeitig zu definieren. Wir können nur den Getter belassen und eine schreibgeschützte Eigenschaft erhalten. Oder wir können dem Setter eine zusätzliche Aktion hinzufügen und den Wert festlegen, z. B. Protokollierung.
    Zusätzlich zu den get / set-Eigenschaften müssen Deskriptoren mehrere weitere Eigenschaften konfigurieren:


     const obj = {}; //      get/set,       value.    get/set  value.   — undefined. Object.defineProperty(obj, 'name', { value: '' }); // ,         (for-in, for-of, Object.keys).   — false. Object.defineProperty(obj, 'a', { enumerable: true }); //         defineProperty     delete.   — false. Object.defineProperty(obj, 'b', { configurable: false }); //      .   — false. Object.defineProperty(obj, 'c', { writable: true }); 

  • Object.getOwnPropertyDescriptor und Object.getOwnPropertyDescriptors - Mit dieser Option können Sie den gewünschten Objektdeskriptor oder dessen vollständige Liste abrufen :


     const obj = { a: 1, b: 2 }; console.log(Object.getOwnPropertyDescriptor(obj, "a")); // { configurable: true, enumerable: true, value: 1, writable: true } /** * { * a: { configurable: true, enumerable: true, value: 1, writable: true }, * b: { configurable: true, enumerable: true, value: 2, writable: true } * } */ console.log(Object.getOwnPropertyDescriptors(obj)); 


Erstellen von Einschränkungen beim Arbeiten mit Objekten


  • Object.freeze - "friert" die Eigenschaften eines Objekts ein. Die Folge eines solchen "Einfrierens" ist die vollständige Unveränderlichkeit der Objekteigenschaften - sie können nicht geändert und gelöscht, neue hinzugefügt, Deskriptoren geändert werden:


     const obj = Object.freeze({ a: 1 }); //       ,       . obj.a = 2; obj.b = 3; console.log(obj); // { a: 1 } console.log(Object.isFrozen(obj)) // true 

  • Object.seal - "versiegelt" die Eigenschaften eines Objekts. Die Versiegelung ähnelt Object.freeze, weist jedoch eine Reihe von Unterschieden auf. Wie in Object.freeze verbieten wir das Hinzufügen neuer Eigenschaften, das Löschen vorhandener Eigenschaften und das Ändern ihrer Deskriptoren. Gleichzeitig können wir jedoch auch die Eigenschaftswerte ändern:


     const obj = Object.seal({ a: 1 }); obj.a = 2; //  a   2 //     ,       . obj.b = 3; console.log(obj); // { a: 2 } console.log(Object.isSealed(obj)) // true 

  • Object.preventExtensions - Verbietet das Hinzufügen neuer Eigenschaften / Deskriptoren:


     const obj = Object.preventExtensions({ a: 1 }); obj.a = 2; //       ,       . obj.b = 3; console.log(obj); // { a: 2 } console.log(Object.isExtensible(obj)) // false 


Objektprototypen


  • Object.create - um ein Objekt mit dem im Parameter angegebenen Prototyp zu erstellen. Diese Funktion kann sowohl für die Vererbung von Prototypen als auch für die Erstellung "sauberer" Objekte ohne Eigenschaften aus Object.prototype verwendet werden :


     const pureObj = Object.create(null); 

  • Object.getPrototypeOf und Object.setPrototypeOf - um den Prototyp eines Objekts abzurufen / zu ändern:


     const duck = {}; const bird = {}; Object.setPrototypeOf(duck, bird); console.log(Object.getPrototypeOf(duck) === bird); // true console.log(duck.__proto__ === bird); // true 

  • Object.prototype.isPrototypeOf - Überprüft, ob das aktuelle Objekt in der Prototypenkette eines anderen Objekts enthalten ist:


     const duck = {}; const bird = {}; duck.__proto__ = bird; console.log(bird.isPrototypeOf(duck)); // true 


4. Reflect API


Mit dem Aufkommen von ES6 wurde JavaScript ein globales Reflect- Objekt hinzugefügt, um verschiedene Methoden in Bezug auf Reflexion und Selbstbeobachtung zu speichern.


Die meisten seiner Methoden sind das Ergebnis der Übertragung vorhandener Methoden von globalen Objekten wie Object und Function in einen separaten Namespace mit ein wenig Refactoring für eine komfortablere Verwendung.


Das Übertragen von Funktionen auf das Reflect-Objekt erleichterte nicht nur die Suche nach den erforderlichen Methoden für die Reflexion und führte zu einer besseren Semantik, sondern vermied auch unangenehme Situationen, in denen unser Objekt keinen Object.prototype in seinem Prototyp enthält, sondern wir die folgenden Methoden verwenden möchten:


 let obj = Object.create(null); obj.qwerty = 'qwerty'; console.log(obj.__proto__) // null console.log(obj.hasOwnProperty('qwerty')) // Uncaught TypeError: obj.hasOwnProperty is not a function console.log(obj.hasOwnProperty === undefined); // true console.log(Object.prototype.hasOwnProperty.call(obj, 'qwerty')); // true 

Refactoring hat das Verhalten von Methoden expliziter und eintöniger gemacht. Wenn beispielsweise früher beim Aufrufen von Object.defineProperty für einen falschen Wert (wie eine Zahl oder eine Zeichenfolge) eine Ausnahme ausgelöst wurde, während beim Aufrufen von Object.getOwnPropertyDescriptor für einen nicht vorhandenen Objektdeskriptor stillschweigend undefiniert zurückgegeben wurde, lösen ähnliche Methoden von Reflect immer Ausnahmen für falsche Daten aus .


Es wurden auch mehrere neue Methoden hinzugefügt:


  • Reflect.construct ist eine bequemere Alternative zu Object.create , mit der ein Objekt mit dem angegebenen Prototyp nicht nur erstellt, sondern auch sofort initialisiert werden kann:


     function Person(name, surname) { this.name = this.formatParam(name); this.surname = this.formatParam(surname); } Person.prototype.formatParam = function(param) { return param.slice(0, 1).toUpperCase() + param.slice(1).toLowerCase(); } const oldPerson = Object.create(Person.prototype); // {} Person.call(oldPerson, '', ''); // {name: "", surname: ""} const newPerson = Reflect.construct(Person, ['', '']); // {name: "", surname: ""} 

  • Reflect.ownKeys - Gibt ein Array von Eigenschaften zurück, die zum angegebenen Objekt gehören (und nicht zu Objekten in der Prototypenkette):


     let person = { name: '', surname: '' }; person.__proto__ = { age: 30 }; console.log(Reflect.ownKeys(person)); // ["name", "surname"] 

  • Reflect.deleteProperty - eine Alternative zum Löschoperator in Form einer Methode:


     let person = { name: '', surname: '' }; delete person.name; // person = {surname: ""} Reflect.deleteProperty(person, 'surname'); // person = {} 

  • Reflect.has - eine Alternative zum in- Operator in Form einer Methode:


     let person = { name: '', surname: '' }; console.log('name' in person); // true console.log(Reflect.has(person, 'name')); // true 

  • Reflect.get und Reflect.set - zum Lesen / Ändern von Objekteigenschaften:


     let person = { name: '', surname: '' }; console.log(Reflect.get(person, 'name')); //  Reflect.set(person, 'surname', '') // person = {name: "", surname: ""} 


Weitere Details zu den Änderungen finden Sie hier .


Metadaten widerspiegeln


Zusätzlich zu den oben aufgeführten Reflect-Objektmethoden gibt es einen experimentellen Vorschlag zum bequemen Binden verschiedener Metadaten an Objekte.


Metadaten können nützliche Informationen sein, die nicht direkt mit dem Objekt zusammenhängen, zum Beispiel:


  • Wenn TypeScript das Flag emitDecoratorMetadata aktiviert, werden Informationen zu Typen in die Metadaten geschrieben, sodass Sie zur Laufzeit darauf zugreifen können. Ferner können diese Informationen durch das Schlüsseldesign erhalten werden: Typ:
     const typeData = Reflect.getMetadata("design:type", object, propertyName); 
  • Die beliebte InversifyJS- Bibliothek zur Inversionssteuerung speichert verschiedene Informationen zu den beschriebenen Beziehungen in den Metadaten.

Momentan wird diese Polyfüllung verwendet, um in Browsern zu arbeiten .


5. Symbole


Symbole sind ein neuer unveränderlicher Datentyp, der hauptsächlich zum Erstellen eindeutiger Namen für Objekteigenschaftsbezeichner verwendet wird. Wir haben die Möglichkeit, Charaktere auf zwei Arten zu erstellen:


  1. Lokale Symbole - Der Text in den Parametern der Symbolfunktion hat keinen Einfluss auf die Eindeutigkeit und wird nur zum Debuggen benötigt:


     const sym1 = Symbol('name'); const sym2 = Symbol('name'); console.log(sym1 == sym2); // false 

  2. Globale Zeichen - Zeichen werden in der globalen Registrierung gespeichert, sodass Zeichen mit demselben Schlüssel gleich sind:


     const sym3 = Symbol.for('name'); const sym4 = Symbol.for('name'); const sym5 = Symbol.for('other name'); console.log(sym3 == sym4); // true,        'name' console.log(sym3 == sym5); // false,     


Die Fähigkeit, solche Bezeichner zu erstellen, ermöglicht es uns, keine Angst zu haben, dass wir eine Eigenschaft in einem uns unbekannten Objekt überschreiben könnten. Diese Qualität ermöglicht es den Erstellern des Standards, Objekten auf einfache Weise neue Standardeigenschaften hinzuzufügen, ohne die Kompatibilität mit verschiedenen vorhandenen Bibliotheken (die möglicherweise dieselbe Eigenschaft definieren) und Benutzercode zu beeinträchtigen. Daher gibt es eine Reihe von Standardsymbolen, von denen einige neue Möglichkeiten zur Reflexion bieten:


  • Symbol.iterator - Ermöglicht das Erstellen eigener Regeln für das Iterieren von Objekten mit dem Operator " for of of or ..." :


     let arr = [1, 2, 3]; //       arr[Symbol.iterator] = function() { const self = this; let pos = this.length - 1; return { next() { if (pos >= 0) { return { done: false, value: self[pos--] }; } else { return { done: true }; } } } }; console.log([...arr]); // [3, 2, 1] 

  • Symbol.hasInstance ist eine Methode, die bestimmt, ob ein Konstruktor ein Objekt als seine Instanz erkennt. Wird von der Instanz des Operators verwendet:


     class MyArray { static [Symbol.hasInstance](instance) { return Array.isArray(instance); } } console.log([] instanceof MyArray); // true 

  • Symbol.isConcatSpreadable - Gibt an, ob das Array bei Verkettung in Array.concat abgeflacht werden soll:


     let firstArr = [1, 2, 3]; let secondArr = [4, 5, 6]; firstArr.concat(secondArr); // [1, 2, 3, 4, 5, 6] secondArr[Symbol.isConcatSpreadable] = false; console.log(firstArr.concat(secondArr)); // [1, 2, 3, [4, 5, 6]] 

  • Symbol.species - Mit dieser Option können Sie angeben, mit welchem ​​Konstruktor abgeleitete Objekte innerhalb der Klasse erstellt werden sollen.
    Zum Beispiel haben wir eine Standard-Array-Klasse für die Arbeit mit Arrays und eine .map-Methode, die ein neues Array basierend auf dem aktuellen Array erstellt. Um herauszufinden, mit welcher Klasse dieses neue Array erstellt werden soll, ruft Array this.constructor [Symbol.species] folgendermaßen auf :


     Array.prototype.map = function(cb) { const ArrayClass = this.constructor[Symbol.species]; const result = new ArrayClass(this.length); this.forEach((value, index, arr) => { result[index] = cb(value, index, arr); }); return result; } 

    Wenn Sie Symbol.species überschreiben, können Sie eine eigene Klasse für die Arbeit mit Arrays erstellen und sagen, dass alle Standardmethoden wie .map, .reduce usw. keine Instanz der Array-Klasse, sondern eine Instanz unserer Klasse zurückgeben:


     class MyArray extends Array { static get [Symbol.species]() { return this; } } const arr = new MyArray(1, 2, 3); // [1, 2, 3] console.log(arr instanceof MyArray); // true console.log(arr instanceof Array); // true //   Array.map     Array,    Symbol.species  this      MyArray const doubledArr = arr.map(x => x * 2); console.log(doubledArr instanceof MyArray); // true console.log(doubledArr instanceof Array); // true 

    Dies funktioniert natürlich nicht nur mit Arrays, sondern auch mit anderen Standardklassen. Selbst wenn wir einfach unsere eigene Klasse mit Methoden erstellen, die neue Instanzen derselben Klasse zurückgeben, sollten wir this.constructor [Symbol.species] verwenden, um einen Verweis auf den Konstruktor zu erhalten.


  • Symbol.toPrimitive - Mit dieser Option können Sie festlegen, wie unser Objekt in einen primitiven Wert konvertiert werden soll. Wenn wir früher, um auf ein Grundelement zu reduzieren, toString zusammen mit valueOf verwenden mussten, kann jetzt alles auf eine bequeme Methode ausgeführt werden:


     const figure = { id: 1, name: '', [Symbol.toPrimitive](hint) { if (hint === 'string') { return this.name; } else if (hint === 'number') { return this.id; } else { // default return this.name; } } } console.log(`${figure}`); // hint = string console.log(+figure); // hint = number console.log(figure + ''); // hint = default 

  • Symbol.match - Ermöglicht das Erstellen eigener Handlerklassen für die Methode für die Funktion String.prototype.match :


     class StartAndEndsWithMatcher { constructor(value) { this.value = value; } [Symbol.match](str) { const startsWith = str.startsWith(this.value); const endsWith = str.endsWith(this.value); if (startsWith && endsWith) { return [this.value]; } return null; } } const testMatchResult = '||'.match(new StartAndEndsWithMatcher('|')); console.log(testMatchResult); // ["|"] const catMatchResult = '|'.match(new StartAndEndsWithMatcher('|')); console.log(catMatchResult) // null 

    Symbol.replace , Symbol.search Symbol.split String.prototype .



, ( reflect-metadata ) . - , , . :


 const validationRules = Symbol('validationRules'); const person = { name: '', surname: '' }; person[validationRules] = { name: ['max-length-256', 'required'], surname: ['max-length-256'] }; 

6. (Proxy)


Proxy , Reflect API Symbols ES6, // , , . , .


, data-binding MobX React, Vue . .


:


 const formData = { login: 'User', password: 'pass' }; const proxyFormData = new Proxy(formData, { set(target, name, value) { target[name] = value; this.forceUpdate(); //   React- } }); //       forceUpdate()    React proxyFormData.login = 'User2'; //     ,   -      proxyFormData.age = 20; 

, /:


 const formData = { login: 'User', password: 'pass' }; const proxyFormData = {}; for (let param in formData) { Reflect.defineProperty(proxyFormData, `__private__${param}`, { value: formData[param], enumerable: false, configurable: true }); Reflect.defineProperty(proxyFormData, param, { get: function() { return this[`__private__${param}`]; }, set: function(value) { this[`__private__${param}`] = value; this.forceUpdate(); //   React- }, enumerable: true, configurable: true }); } //       forceUpdate()    React proxyFormData.login = 'User2'; //                  -  Reflect.defineProperty proxyFormData.age = 20; 

-, — Proxy ( , ), / , delete obj[name] .


7.


JavaScript , ECMAScript 4, . , .


You Don't Know JS .

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


All Articles