Lernen Sie Metaprogrammierung in JavaScript kennen

Dieser Artikel stammt aus der Huawei Cloud Community „ Metaprogramming to Make Code More Descriptive, Expressive, and Flexible “ von Ye Yiyi.

Hintergrund

In der zweiten Hälfte des letzten Jahres habe ich meinem WeChat-Bücherregal viele technische Bücher in verschiedenen Kategorien hinzugefügt und einige davon zwischendurch gelesen.

Lesen ohne Plan wird kaum Ergebnisse bringen.

Zu Beginn des neuen Jahres bin ich bereit, etwas anderes auszuprobieren, zum Beispiel eine Lesewoche. Nehmen Sie sich jeden Monat 1 bis 2 nicht aufeinanderfolgende Wochen Zeit, um ein Buch vollständig zu lesen.

Obwohl diese Spielanleitung allgemein und starr ist, ist sie für mich seit drei Monaten effektiv.

Für April gibt es zwei Lesepläne und die Reihe „JavaScript You Don't Know“ ist zu Ende.

 

Bereits gelesene Bücher : „Der Weg zur einfachen Architektur“, „Node.js auf einfache und einfache Weise“, „JavaScript, das Sie nicht kennen (Band 1)“, „JavaScript, das Sie nicht kennen (Band 2)“.

 

 

Aktuelles Buch der Lesewoche : „JavaScript You Don't Know (Band 2)“.

 

Metaprogrammierung

Funktionsname

Es gibt viele Möglichkeiten, eine Funktion in einem Programm auszudrücken, und es ist nicht immer klar, wie der „Name“ der Funktion lauten soll.

Noch wichtiger ist, dass wir feststellen müssen, ob der „Name“ der Funktion lediglich ihr Namensattribut ist (ja, Funktionen haben ein Attribut namens „Name“) oder ob er auf ihren lexikalisch gebundenen Namen verweist, wie z. B. Funktionsleiste(){ In .}.

Das Namensattribut wird für Metaprogrammierungszwecke verwendet.

Standardmäßig ist der lexikalische Name der Funktion (falls vorhanden) auch auf ihr Namensattribut festgelegt. Tatsächlich erfordert die ES5-Spezifikation (und frühere Versionen) dieses Verhalten nicht offiziell. Die Einstellung des Namensattributs ist nicht standardmäßig, aber dennoch relativ zuverlässig. Dies wurde in ES6 standardisiert.

In ES6 gibt es jetzt eine Reihe von Ableitungsregeln, die dem Namensattribut einer Funktion sinnvoll einen Wert zuweisen können, auch wenn für die Funktion kein lexikalischer Name verfügbar ist.

Zum Beispiel:

var abc = Funktion () {
  // ..
};

abc.name; // „abc“

Hier sind einige andere Formen der Namensableitung (oder deren Fehlen) in ES6:

(function(){ .. }); // Name:
(function*(){ .. }); // Name:
window.foo = function(){ .. }; // Name:
Klasse Super {
    Konstruktor() { .. } // Name: Großartig
    lustig() { .. } // Name: lustig
}

var c = class Awesome { .. }; // Name: Großartig
var o = {
    foo() { .. }, // Name: foo
    *bar() { .. }, // Name: bar
    baz: () => { .. }, // Name: baz
    bam: function(){ .. }, // Name: bam
    get what() { .. }, // Name: get what
    set fuz() { .. }, // Name: set fuz
    ["b" + "iz"]:
      function(){ .. }, // Name: biz
    [Symbol( "buz" )]:
      function(){ .. } // Name: [buz]
};

var x = o.foo.bind( o ); // Name: gebunden foo
(function(){ .. }).bind( o ); // Name: gebunden
export default function() { .. } // Name: default
var y = new Function(); // Name: anonym
wobei GeneratorFunction =
    Funktion*(){}. proto .constructor;
var z = new GeneratorFunction(); // Name: anonym

Standardmäßig ist die Namenseigenschaft nicht beschreibbar, aber konfigurierbar, was bedeutet, dass sie bei Bedarf manuell mit Object.defineProperty(..) geändert werden kann.

Metaattribut

Metaattribute stellen spezielle Metainformationen in Form von Attributzugriffen bereit, die mit anderen Methoden nicht abgerufen werden können.

Am Beispiel von new.target wird das Schlüsselwort new als Kontext für den Attributzugriff verwendet. Offensichtlich ist neu selbst kein Objekt, daher ist diese Funktion etwas ganz Besonderes. Wenn new.target innerhalb eines Konstruktoraufrufs (einer durch new ausgelösten Funktion/Methode) verwendet wird, wird new zu einem virtuellen Kontext, der es new.target ermöglicht, auf den Zielkonstruktor zu verweisen, der new aufruft.

Dies ist ein klares Beispiel für eine Metaprogrammierungsoperation, da ihr Zweck darin besteht, innerhalb des Konstruktoraufrufs zu bestimmen, was das ursprüngliche neue Ziel war, im Allgemeinen zur Selbstbeobachtung (Überprüfung von Typ/Struktur) oder zum Zugriff auf statische Eigenschaften.

Beispielsweise möchten Sie möglicherweise unterschiedliche Aktionen innerhalb eines Konstruktors ausführen, je nachdem, ob er direkt oder über eine Unterklasse aufgerufen wird:

Klasse Parent {
  Konstrukteur() {
    if (new.target === Parent) {
      console.log('Parent instanziiert');
    } anders {
      console.log('Ein Kind instanziiert');
    }
  }
}

Klasse Child erweitert Parent {}

var a = new Parent();
// Übergeordnetes Element instanziiert

var b = neues Kind();
// Ein Kind instanziiert

Der Konstruktor() innerhalb der Definition der übergeordneten Klasse erhält tatsächlich den lexikalischen Namen der Klasse (übergeordnet), auch wenn die Syntax impliziert, dass die Klasse eine vom Konstruktor getrennte Entität ist.

öffentliches Symbol

JavaScript definiert einige integrierte Symbole vor, die als öffentliche Symbole (Well-Known Symbol, WKS) bezeichnet werden.

Diese Symbole dienen hauptsächlich dazu, spezielle Metaeigenschaften bereitzustellen, sodass diese Metaeigenschaften JavaScript-Programmen zugänglich gemacht werden können, um eine bessere Kontrolle über das JavaScript-Verhalten zu erhalten.

Symbol.iterator

Symbol.iterator stellt einen speziellen Ort (Attribut) auf einem beliebigen Objekt dar. Der Sprachmechanismus findet automatisch eine Methode an diesem Ort. Diese Methode erstellt einen Iterator, um den Wert dieses Objekts zu verwenden. Viele Objektdefinitionen haben einen Standardwert für dieses Symbol.

Sie können jedoch auch Ihre eigene Iteratorlogik für beliebige Objektwerte definieren, indem Sie die Eigenschaft Symbol.iterator definieren, auch wenn diese den Standarditerator überschreibt. Der Aspekt der Metaprogrammierung besteht hier darin, dass wir ein Verhaltensattribut definieren, das von anderen Teilen von JavaScript (z. B. Operatoren und Schleifenkonstrukten) beim Umgang mit dem definierten Objekt verwendet werden kann.

Zum Beispiel:

var Narbe = [4, 5, 6, 7, 8, 9];

for (var v of arr) {
  console.log(v);
}
// 4 5 6 7 8 9

// Definieren Sie einen Iterator, der nur Werte bei ungeraden Indexwerten erzeugt
arr[Symbol.iterator] = function* () {
  wobei idx = 1;
  Tun {
    yield this[idx];
  } while ((idx += 2) < this.length);
};

for (var v of arr) {
  console.log(v);
}
// 5 7 9

Symbol.toStringTag und Symbol.hasInstance

Eine der häufigsten Metaprogrammierungsaufgaben besteht darin, einen Wert zu untersuchen, um herauszufinden, um welche Art es sich handelt, normalerweise um zu bestimmen, welche Operationen für die Ausführung geeignet sind. Für Objekte sind die am häufigsten verwendeten Introspektionsverfahren toString() und instanceof.

In ES6 können Sie das Verhalten dieser Vorgänge steuern:

Funktion Foo(Begrüßung) {
  this.greeting = Begrüßung;
}

Foo.prototype[Symbol.toStringTag] = 'Foo';

Object.defineProperty(Foo, Symbol.hasInstance, {
  Wert: Funktion (inst) {
    return inst.greeting == 'hello';
  },
});

var a = new Foo('hello'),
  b = new Foo('world');

b[Symbol.toStringTag] = 'cool';

a.toString(); // [Objekt Foo]
String(b); // [Objekt cool]
eine Instanz von Foo; // WAHR

b Instanz von Foo; // FALSCH

Die @@toStringTag-Notation des Prototyps (oder der Instanz selbst) gibt den String-Wert an, der verwendet wird, wenn [Objekt] stringifiziert wird.

Die @@hasInstance-Notation ist eine Methode der Konstruktorfunktion, die einen Instanzobjektwert akzeptiert und „true“ oder „false“ zurückgibt, um anzugeben, ob der Wert als Instanz betrachtet werden kann.

Symbol.Arten

Welcher Konstruktor verwendet werden soll (Array(..) oder eine benutzerdefinierte Unterklasse), wenn Sie eine Unterklasse von Array erstellen und geerbte Methoden (z. B. Slice(..)) definieren möchten. Standardmäßig wird durch den Aufruf von Slice(..) für eine Instanz einer Array-Unterklasse eine neue Instanz dieser Unterklasse erstellt.

Diese Anforderung kann metaprogrammiert werden, indem die standardmäßige @@species-Definition einer Klasse überschrieben wird:

Klasse Cool {
  // @@species auf Unterklassen verschieben
  static get [Symbol.species]() {
    gib dies zurück;
  }

  wieder() {
    return new this.constructor[Symbol.species]();
  }
}

Klasse Spaß erweitert Cool {}

Klasse Awesome erweitert Cool {
  //Erzwinge, dass @@species als übergeordneter Konstruktor angegeben wird
  static get [Symbol.species]() {
    zurück Cool;
  }
}

var a = new Fun(),
  b = new Awesome(),
  c = a.again(),
  d = b.again();

c Instanz von Fun; // WAHR
d Instanz von Awesome; // FALSCH
d Instanz von Cool; // WAHR

Das Standardverhalten von Symbol.species bei integrierten nativen Konstruktoren besteht darin, dies zurückzugeben. Es gibt keinen Standardwert für die Benutzerklasse, aber wie gezeigt, lässt sich diese Verhaltensfunktion leicht simulieren.

Wenn Sie eine Methode zum Generieren neuer Instanzen definieren müssen, verwenden Sie die Mustermetaprogrammierung new this.constructor[Symbol.species](..), anstatt new this.constructor(..) oder new XYZ(..) fest zu codieren. Erbende Klassen können dann Symbol.species anpassen, um zu steuern, welcher Konstruktor diese Instanzen generiert.

Schauspielkunst

Eine der offensichtlichsten neuen Metaprogrammierungsfunktionen in ES6 ist die Proxy-Funktion.

Ein Proxy ist ein spezielles Objekt, das Sie erstellen und das ein anderes gewöhnliches Objekt „kapselt“ – oder vor diesem gewöhnlichen Objekt steht. Sie können eine spezielle Verarbeitungsfunktion (d. h. Trap) für das Proxy-Objekt registrieren. Dieses Programm wird aufgerufen, wenn verschiedene Vorgänge auf dem Proxy ausgeführt werden. Diese Handler haben die Möglichkeit, zusätzlich zu den Weiterleitungsvorgängen an das ursprüngliche Ziel/gekapselte Objekt zusätzliche Logik auszuführen.

Ein Beispiel für eine Trap-Handler-Funktion, die Sie auf einem Proxy definieren können, ist get, die den Vorgang [[Get]] abfängt, wenn Sie versuchen, auf die Eigenschaften eines Objekts zuzugreifen.

var obj = { a: 1 },
  Handler = {
    get(Ziel, Schlüssel, Kontext) {
      // Hinweis: target === obj,
      // Kontext === pobj
      console.log('Zugriff auf: ', Schlüssel);
      return Reflect.get(target, key, context);
    },
  },
  pobj = new Proxy(obj, handler);

obj.a;
// 1
pobj.a;
// Zugriff auf: a
// 1

Wir deklarieren eine get(..)-Verarbeitungsfunktionsbenennungsmethode für das Handler-Objekt (der zweite Parameter von Proxy(..)), die eine Zielobjektreferenz (obj), einen Schlüsselattributnamen („a“) und Körperliterale akzeptiert Selbst/Empfänger/Agent (pobj).

Einschränkungen der Agentur

Über diese Metaprogrammierungs-Funktionsfallen kann eine Vielzahl grundlegender Operationen abgewickelt werden, die an Objekten ausgeführt werden können. Es gibt jedoch einige Operationen, die (zumindest im Moment) nicht abgefangen werden können.

var obj = { a:1, b:2 },
Handler = { .. },
pobj = new Proxy( obj, handler );
Typ des Objekts;
String( obj );

obj + "";
obj == pobj;
obj === pobj

Zusammenfassen

Fassen wir den Hauptinhalt dieses Artikels zusammen:

  • Vor ES6 verfügte JavaScript bereits über viele Metaprogrammierungsfunktionen, und ES6 bietet mehrere neue Funktionen, die die Metaprogrammierungsfunktionen erheblich verbessern.
  • Von der Ableitung von Funktionsnamen für anonyme Funktionen bis hin zu Metaeigenschaften, die Informationen darüber liefern, wie ein Konstruktor aufgerufen wird, können Sie tiefer als je zuvor in die Struktur der Laufzeit Ihres Programms blicken. Durch das Offenlegen von Symbolen können Sie Originalfunktionen überschreiben, beispielsweise die Typkonvertierung von Objekten in native Typen. Proxys können verschiedene zugrunde liegende Vorgänge von Objekten abfangen und anpassen, und Reflect stellt Tools zur Simulation dieser Vorgänge bereit.
  • Der ursprüngliche Autor empfiehlt: Konzentrieren Sie sich zunächst darauf, zu verstehen, wie der Kernmechanismus dieser Sprache funktioniert. Und wenn Sie erst einmal wirklich verstanden haben, wie JavaScript selbst funktioniert, ist es an der Zeit, diese leistungsstarken Metaprogrammierungsfunktionen zu nutzen, um die Sprache weiter anzuwenden.

Klicken Sie hier, um zu folgen und so schnell wie möglich mehr über die neuen Technologien von Huawei Cloud zu erfahren~

Linus nahm die Sache selbst in die Hand, um zu verhindern, dass Kernel-Entwickler Tabulatoren durch Leerzeichen ersetzen. Sein Vater ist einer der wenigen Führungskräfte, die Code schreiben können, sein zweiter Sohn ist Direktor der Open-Source-Technologieabteilung und sein jüngster Sohn ist ein Kern Mitwirkender bei Open Source: Es dauerte ein Jahr, 5.000 häufig verwendete mobile Anwendungen zu konvertieren. Java ist die Sprache, die am anfälligsten für Schwachstellen von Drittanbietern ist. Wang Chenglu, der Vater von Hongmeng: Open Source Hongmeng ist die einzige architektonische Innovation im Bereich der Basissoftware in China. Ma Huateng und Zhou Hongyi geben sich die Hand, um „den Groll zu beseitigen.“ Ehemaliger Microsoft-Entwickler: Die Leistung von Windows 11 ist „lächerlich schlecht“. sind sehr herzerwärmend . Meta Llama 3 wird offiziell veröffentlicht
{{o.name}}
{{m.name}}

Ich denke du magst

Origin my.oschina.net/u/4526289/blog/11054218
Empfohlen
Rangfolge