Heim >Web-Frontend >js-Tutorial >JavaScript neu denken. Teilweise Anwendung, referenzielle Transparenz und verzögerte Operationen

JavaScript neu denken. Teilweise Anwendung, referenzielle Transparenz und verzögerte Operationen

Susan Sarandon
Susan SarandonOriginal
2024-12-28 17:34:38416Durchsuche

Rethinking JavaScript. Partial Application, Referential Transparency, and Lazy Operations

Hallo Leute! Als ich vor einiger Zeit die neuesten TC39-Vorschläge durchstöberte, stieß ich auf eines, das mich begeisterte – und ein wenig skeptisch machte. Es geht um eine teilweise Anwendungssyntax für JavaScript. Auf den ersten Blick scheint es die perfekte Lösung für viele häufig auftretende Programmierprobleme zu sein, aber als ich darüber nachdachte, wurde mir klar, dass es sowohl viel zu mögen als auch Raum für Verbesserungen gibt.  

Noch besser: Diese Bedenken lösten eine völlig neue Idee aus, die JavaScript noch leistungsfähiger machen könnte. Lassen Sie mich Sie auf diese Reise mitnehmen, komplett mit realistischen Beispielen, wie diese Funktionen die Art und Weise, wie wir jeden Tag programmieren, verändern könnten.

TLDR: Der Artikel stammt aus meiner alten Ausgabe zum Vorschlag: https://github.com/tc39/proposal-partial-application/issues/53


Der Vorschlag

Mit der teilweisen Anwendung können Sie einige Argumente einer Funktion „voreinstellen“ und so eine neue Funktion zur späteren Verwendung zurückgeben. Unser aktueller Code sieht so aus:

const fetchWithAuth = (path: string) => fetch(
  { headers: { Authorization: "Bearer token" } },
  path,
);
fetchWithAuth("/users");
fetchWithAuth("/posts");

Der Vorschlag führt hierfür eine ~()-Syntax ein:

const fetchWithAuth = fetch~({ headers: { Authorization: "Bearer token" } }, ?);
fetchWithAuth("/users");
fetchWithAuth("/posts");

Sehen Sie, was passiert? Die fetchWithAuth-Funktion füllt das Header-Argument vorab aus, sodass Sie nur die URL angeben müssen. Es ist wie .bind(), aber flexibler und einfacher zu lesen.

Der Vorschlag ermöglicht Ihnen auch die Verwendung von ? als Platzhalter für unbefüllte Argumente und ... für einen Restparameter. Zum Beispiel:

const sendEmail = send~(user.email, ?, ...);
sendEmail("Welcome!", "Hello and thanks for signing up!");
sendEmail("Reminder", "Don't forget to confirm your email.");

Mir gefällt am besten, dass ich die Typanmerkungen nicht duplizieren muss!

Klingt nützlich, oder? Aber es gibt noch viel mehr zu entpacken.


Das Argument für referenzielle Transparenz

Beginnen wir mit einem praktischen Problempunkt: Funktionsabschlüsse und veraltete Variablenreferenzen.

Angenommen, Sie planen eine Benachrichtigung. Sie könnten so etwas schreiben:

function notify(state: { data?: Data }) {
  if (state.data) {
      setTimeout(() => alert(state.data), 1000)
  }
}

Haben Sie das Problem bereits gesehen? Die Eigenschaft „data“ kann sich während des Timeouts ändern und die Warnung zeigt nichts an! Um dieses Problem zu beheben, muss die Wertreferenz explizit übergeben werden. Hoffentlich akzeptiert „setTimeout“ zusätzliche Argumente, um sie an den Rückruf zu übergeben:

function notify(state: { data?: Data }) {
  if (state.data) {
      setTimeout((data) => alert(data), 1000, state.data)
  }
}

Nicht schlecht, aber es wird nicht allgemein von allen APIs unterstützt. Eine teilweise Anwendung könnte dieses Muster weitaus universeller machen:

function notify(state: { data?: Data }) {
  if (state.data) {
      setTimeout(alert~(state.data), 1000)
  }
}

Durch das Sperren von state.data zum Zeitpunkt der Funktionserstellung vermeiden wir unerwartete Fehler aufgrund veralteter Referenzen.


Reduzierung wiederholter Berechnungen

Ein weiterer praktischer Vorteil der Teilanwendung besteht darin, dass redundante Arbeiten bei der Verarbeitung großer Datensätze entfallen.

Zum Beispiel haben Sie eine Mapping-Logik, die für jeden Iterationsschritt zusätzliche Daten berechnen muss:

const fetchWithAuth = (path: string) => fetch(
  { headers: { Authorization: "Bearer token" } },
  path,
);
fetchWithAuth("/users");
fetchWithAuth("/posts");

Das Problem liegt im Proxy-Zugriff auf this.some.another. Es ist ziemlich schwer, jeden Iterationsschritt aufzurufen. Es wäre besser, diesen Code wie folgt umzugestalten:

const fetchWithAuth = fetch~({ headers: { Authorization: "Bearer token" } }, ?);
fetchWithAuth("/users");
fetchWithAuth("/posts");

Mit der teilweisen Anwendung können wir es weniger ausführlich machen:

const sendEmail = send~(user.email, ?, ...);
sendEmail("Welcome!", "Hello and thanks for signing up!");
sendEmail("Reminder", "Don't forget to confirm your email.");

Durch das Einbinden gemeinsamer Berechnungen machen Sie den Code prägnanter und leichter verständlich, ohne dass die Leistung darunter leidet.


Warum neue Syntax hinzufügen?

Hier fing ich an, mir den Kopf zu zerbrechen. Während die vorgeschlagene Syntax elegant ist, verfügt JavaScript bereits über viele Operatoren. Besonders die Fragezeichenoperatoren? Das Hinzufügen von ~() kann das Erlernen und Parsen der Sprache erschweren.

Was wäre, wenn wir die gleiche Funktionalität erreichen könnten, ohne eine neue Syntax einzuführen?


Eine methodenbasierte Alternative

Stellen Sie sich vor, Function.prototype mit einer Tie-Methode zu erweitern:

function notify(state: { data?: Data }) {
  if (state.data) {
      setTimeout(() => alert(state.data), 1000)
  }
}

Es ist etwas ausführlicher, vermeidet aber die Einführung eines völlig neuen Operators. Durch ein zusätzliches Sonderzeichen für Platzhalter können wir das Fragezeichen ersetzen.

function notify(state: { data?: Data }) {
  if (state.data) {
      setTimeout((data) => alert(data), 1000, state.data)
  }
}

Es ist ein perfektes Polypiling ohne zusätzliche Komplexität in der Bauzeit!

function notify(state: { data?: Data }) {
  if (state.data) {
      setTimeout(alert~(state.data), 1000)
  }
}

Aber das ist nur die Spitze des Eisbergs. Dadurch wird das Platzhalterkonzept über verschiedene APIs hinweg wiederverwendbar.


Lazy Operations: Noch weiter gehen

Hier wird es richtig interessant. Was wäre, wenn wir das Symbolkonzept erweitern würden, um Lazy Operations zu ermöglichen?

Beispiel 1: Kombination von .filter() und .map()

Angenommen, Sie bearbeiten eine Produktliste für eine E-Commerce-Website. Sie möchten nur reduzierte Artikel mit gerundeten Preisen anzeigen. Normalerweise würden Sie Folgendes schreiben:

class Store  {
  data: { list: [], some: { another: 42 } }
  get computedList() {
    return this.list.map((el) => computeElement(el, this.some.another))
  }
  contructor() {
    makeAutoObservable(this)
  }
}

Dies erfordert jedoch eine zweimalige Iteration über das Array. Mit Lazy Operations könnten wir beide Schritte in einem Durchgang kombinieren:

class Store  {
  data: { list: [], some: { another: 42 } }
  get computedList() {
    const { another } = this.some
    return this.list.map((el) => computeElement(el, another))
  }
  contructor() {
    makeAutoObservable(this)
  }
}

Symbol.skip weist die Engine an, Elemente aus dem endgültigen Array auszuschließen, wodurch der Vorgang sowohl effizient als auch ausdrucksstark wird!

Beispiel 2: Vorzeitige Beendigung in .reduce()

Stellen Sie sich vor, Sie berechnen den Gesamtumsatz aus den ersten fünf Verkäufen. Normalerweise würden Sie eine Bedingung innerhalb von .reduce():
verwenden

class Store  {
  data: { list: [], some: { another: 42 } }
  get computedList() {
    return this.list.map(computeElement~(?, this.some.another))
  }
  contructor() {
    makeAutoObservable(this)
  }
}

Das funktioniert, verarbeitet aber trotzdem jedes Element im Array. Mit verzögerten Kürzungen könnten wir eine vorzeitige Kündigung signalisieren:

function notify(state: { data?: Data }) {
  if (state.data) {
      setTimeout(alert.tie(state.data), 1000)
  }
}

Das Vorhandensein von Symbol.skip könnte der Engine mitteilen, dass sie die Iteration stoppen soll, sobald die Bedingung erfüllt ist, wodurch wertvolle Zyklen eingespart werden.


Warum das wichtig ist

Diese Ideen – teilweise Anwendung, referenzielle Transparenz und verzögerte Operationen – sind nicht nur akademische Konzepte. Sie lösen reale Probleme:

  • Sauberere API-Nutzung: Argumente im Voraus sperren und veraltete Referenzen vermeiden.
  • Verbesserte Leistung: Eliminieren Sie redundante Berechnungen und ermöglichen Sie eine effizientere Iteration.
  • Größere Ausdruckskraft:Schreiben Sie prägnanten, deklarativen Code, der einfacher zu lesen und zu warten ist.

Ob wir bei ~() bleiben oder Alternativen wie tie und Symbol.skip erkunden, die zugrunde liegenden Prinzipien haben ein enormes Potenzial, die Art und Weise, wie wir JavaScript schreiben, zu verbessern.

Ich stimme für den Symbolansatz, da er leicht mehrfach auszufüllen ist und verschiedene Verwendungsmöglichkeiten bietet.


Was kommt als nächstes?

Ich bin neugierig – was denkst du? Ist ~() die richtige Richtung oder sollten wir methodenbasierte Ansätze erkunden? Und wie würden sich Lazy Operations auf Ihren Arbeitsablauf auswirken? Lasst uns in den Kommentaren diskutieren!

Das Schöne an JavaScript liegt in seiner von der Community vorangetriebenen Entwicklung. Indem wir Ideen austauschen und diskutieren, können wir eine Sprache entwickeln, die für alle besser funktioniert. Lassen Sie uns das Gespräch am Laufen halten!

Das obige ist der detaillierte Inhalt vonJavaScript neu denken. Teilweise Anwendung, referenzielle Transparenz und verzögerte Operationen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn