Abschlüsse, Curry-Funktionen und coole Abstraktionen in JavaScript

In diesem Artikel werden wir über Verschlüsse und Curry-Funktionen sprechen und mit diesen Konzepten herumspielen, um coole Abstraktionen zu erstellen. Ich möchte die Idee hinter jedem Konzept zeigen, es aber auch mit Beispielen und überarbeitetem Code sehr praktisch machen, damit es mehr Spaß macht.

Verschlüsse

Abschlüsse sind ein häufiges Thema in JavaScript, mit dem wir beginnen werden. Laut MDN:

Ein Abschluss ist die Kombination einer Funktion, die zusammengebunden (eingeschlossen) mit Verweisen auf ihren Umgebungszustand (die lexikalische Umgebung) ist.

Grundsätzlich wird jedes Mal, wenn eine Funktion erstellt wird, auch ein Abschluss erstellt, der Zugriff auf den Status (Variablen, Konstanten, Funktionen usw.) bietet. Der umliegende Staat ist bekannt als der lexical environment.

Lassen Sie uns ein einfaches Beispiel zeigen:

function makeFunction() { const name = 'TK'; function displayName() { console.log(name); } return displayName; }; 

Was haben wir hier?

  • Unsere Hauptfunktion heißt makeFunction
  • Eine Konstante mit dem Namen namewird der Zeichenfolge zugewiesen.'TK'
  • Die Definition der displayNameFunktion (die nur die nameKonstante protokolliert )
  • Und schließlich wird makeFunctiondie displayNameFunktion zurückgegeben

Dies ist nur eine Definition einer Funktion. Wenn wir das aufrufen makeFunction, wird alles darin erzeugt: eine Konstante und in diesem Fall eine andere Funktion.

Wie wir wissen, wird beim Erstellen der displayNameFunktion auch der Abschluss erstellt, und die Funktion wird auf ihre Umgebung aufmerksam gemacht, in diesem Fall auf die nameKonstante. Deshalb können wir console.logdie nameKonstante, ohne etwas zu brechen. Die Funktion kennt die lexikalische Umgebung.

const myFunction = makeFunction(); myFunction(); // TK 

Toll! Es funktioniert wie erwartet. Der Rückgabewert von makeFunctionist eine Funktion, die wir in der myFunctionKonstante speichern . Wenn wir anrufen myFunction, wird es angezeigt TK.

Wir können es auch als Pfeilfunktion verwenden:

const makeFunction = () => { const name = 'TK'; return () => console.log(name); }; 

Aber was ist, wenn wir den Namen weitergeben und anzeigen wollen? Einfach! Verwenden Sie einen Parameter:

const makeFunction = (name = 'TK') => { return () => console.log(name); }; // Or as a one-liner const makeFunction = (name = 'TK') => () => console.log(name); 

Jetzt können wir mit dem Namen spielen:

const myFunction = makeFunction(); myFunction(); // TK const myFunction = makeFunction('Dan'); myFunction(); // Dan 

myFunction ist sich des übergebenen Arguments bewusst und ob es sich um einen Standardwert oder einen dynamischen Wert handelt.

Durch das Schließen wird sichergestellt, dass die erstellte Funktion nicht nur die Konstanten / Variablen, sondern auch andere Funktionen innerhalb der Funktion kennt.

Das funktioniert also auch:

const makeFunction = (name = 'TK') => { const display = () => console.log(name); return () => display(); }; const myFunction = makeFunction(); myFunction(); // TK 

Die zurückgegebene Funktion kennt die displayFunktion und kann sie aufrufen.

Eine leistungsstarke Technik besteht darin, Verschlüsse zu verwenden, um "private" Funktionen und Variablen zu erstellen.

Vor Monaten habe ich (wieder!) Datenstrukturen gelernt und wollte jede implementieren. Aber ich habe immer den objektorientierten Ansatz verwendet. Als Enthusiast der funktionalen Programmierung wollte ich alle Datenstrukturen nach FP-Prinzipien aufbauen (reine Funktionen, Unveränderlichkeit, referenzielle Transparenz usw.).

Die erste Datenstruktur, die ich lernte, war der Stapel. Es ist ziemlich einfach. Die Haupt-API ist:

  • push: Fügen Sie ein Element an der ersten Stelle des Stapels hinzu
  • pop: Entfernen Sie das erste Element vom Stapel
  • peek: Holen Sie sich den ersten Gegenstand vom Stapel
  • isEmpty: Überprüfen Sie, ob der Stapel leer ist
  • size: Ermittelt die Anzahl der Elemente, die der Stapel enthält

Wir könnten eindeutig eine einfache Funktion für jede "Methode" erstellen und die Stapeldaten an sie übergeben. Es könnte dann die Daten verwenden / transformieren und zurückgeben.

Wir können aber auch einen Stapel mit privaten Daten erstellen und nur die API-Methoden verfügbar machen. Lass uns das machen!

const buildStack = () => { let items = []; const push = (item) => items = [item, ...items]; const pop = () => items = items.slice(1); const peek = () => items[0]; const isEmpty = () => !items.length; const size = () => items.length; return { push, pop, peek, isEmpty, size, }; }; 

Da wir den itemsStapel in unserer buildStackFunktion erstellt haben, ist er "privat". Es kann nur innerhalb der Funktion zugegriffen werden. In diesem Fall ist nur push, popund so könnte man die Daten berühren. Genau das suchen wir.

Und wie benutzen wir es? So was:

const stack = buildStack(); stack.isEmpty(); // true stack.push(1); // [1] stack.push(2); // [2, 1] stack.push(3); // [3, 2, 1] stack.push(4); // [4, 3, 2, 1] stack.push(5); // [5, 4, 3, 2, 1] stack.peek(); // 5 stack.size(); // 5 stack.isEmpty(); // false stack.pop(); // [4, 3, 2, 1] stack.pop(); // [3, 2, 1] stack.pop(); // [2, 1] stack.pop(); // [1] stack.isEmpty(); // false stack.peek(); // 1 stack.pop(); // [] stack.isEmpty(); // true stack.size(); // 0 

Wenn der Stapel erstellt wird, kennen alle Funktionen die itemsDaten. Außerhalb der Funktion können wir jedoch nicht auf diese Daten zugreifen. Es ist privat. Wir ändern die Daten nur mithilfe der integrierten API des Stacks.

Curry

"Beim Currying wird eine Funktion mit mehreren Argumenten in eine Folge von Funktionen mit jeweils nur einem Argument umgewandelt."

- Frontend-Interview

Stellen Sie sich vor, Sie haben eine Funktion mit mehreren Argumenten : f(a, b, c). Mit Curry erreichen wir eine Funktion f(a), die eine Funktion g(b)zurückgibt, die eine Funktion zurückgibt h(c).

Grundsätzlich gilt: f(a, b, c)->f(a) => g(b) => h(c)

Lassen Sie uns ein einfaches Beispiel erstellen, das zwei Zahlen hinzufügt. Aber zuerst ohne Curry:

const add = (x, y) => x + y; add(1, 2); // 3 

Toll! Super einfach! Hier haben wir eine Funktion mit zwei Argumenten. Um es in eine Curry-Funktion umzuwandeln, benötigen wir eine Funktion, die xeine Funktion empfängt yund zurückgibt, die die Summe beider Werte empfängt und zurückgibt.

const add = (x) => { function addY(y) { return x + y; } return addY; }; 

Wir können addYeine anonyme Pfeilfunktion umgestalten :

const add = (x) => { return (y) => { return x + y; } }; 

Oder vereinfachen Sie es, indem Sie Funktionen für einen Pfeil erstellen:

const add = (x) => (y) => x + y; 

Diese drei verschiedenen Curry-Funktionen haben dasselbe Verhalten: Erstellen Sie eine Folge von Funktionen mit nur einem Argument.

Wie können wir es benutzen?

add(10)(20); // 30 

Anfangs kann es etwas seltsam aussehen, aber dahinter steckt eine Logik. add(10)gibt eine Funktion zurück. Und wir nennen diese Funktion mit dem 20Wert.

Dies ist das gleiche wie:

const addTen = add(10); addTen(20); // 30 

Und das ist interessant. Wir können spezielle Funktionen generieren, indem wir die erste Funktion aufrufen. Stellen Sie sich vor, wir wollen eine incrementFunktion. Wir können es aus unserer addFunktion generieren, indem wir es 1als Wert übergeben.

const increment = add(1); increment(9); // 10 

Als ich Lazy Cypress implementierte, eine npm-Bibliothek zum Aufzeichnen des Benutzerverhaltens auf einer Formularseite und zum Generieren von Cypress-Testcode, wollte ich eine Funktion zum Generieren dieser Zeichenfolge erstellen input[data-testid="123"]. Also hatte ich das Element ( input), das Attribut ( data-testid) und den Wert ( 123). Das Interpolieren dieser Zeichenfolge in JavaScript würde folgendermaßen aussehen : ${element}[${attribute}="${value}"].

My first implementation was to receive these three values as parameters and return the interpolated string above:

const buildSelector = (element, attribute, value) => `${element}[${attribute}="${value}"]`; buildSelector('input', 'data-testid', 123); // input[data-testid="123"] 

And it was great. I achieved what I was looking for.

But at the same time, I wanted to build a more idiomatic function. Something where I could write "Get element X with attribute Y and value Z". So if we break this phrase into three steps:

  • "get an element X": get(x)
  • "with attribute Y": withAttribute(y)
  • "and value Z": andValue(z)

We can transform buildSelector(x, y, z) into get(x)withAttribute(y)andValue(z) by using the currying concept.

const get = (element) => { return { withAttribute: (attribute) => { return { andValue: (value) => `${element}[${attribute}="${value}"]`, } } }; }; 

Here we use a different idea: returning an object with function as key-value. Then we can achieve this syntax: get(x).withAttribute(y).andValue(z).

And for each returned object, we have the next function and argument.

Refactoring time! Remove the return statements:

const get = (element) => ({ withAttribute: (attribute) => ({ andValue: (value) => `${element}[${attribute}="${value}"]`, }), }); 

I think it looks prettier. And here's how we use it:

const selector = get('input') .withAttribute('data-testid') .andValue(123); selector; // input[data-testid="123"] 

The andValue function knows about the element and attribute values because it is aware of the lexical environment like with closures that we talked about before.

We can also implement functions using "partial currying" by separating the first argument from the rest for example.

After doing web development for a long time, I am really familiar with the event listener Web API. Here's how to use it:

const log = () => console.log('clicked'); button.addEventListener('click', log); 

I wanted to create an abstraction to build specialized event listeners and use them by passing the element and a callback handler.

const buildEventListener = (event) => (element, handler) => element.addEventListener(event, handler); 

This way I can create different specialized event listeners and use them as functions.

const onClick = buildEventListener('click'); onClick(button, log); const onHover = buildEventListener('hover'); onHover(link, log); 

With all these concepts, I could create an SQL query using JavaScript syntax. I wanted to query JSON data like this:

const json = { "users": [ { "id": 1, "name": "TK", "age": 25, "email": "[email protected]" }, { "id": 2, "name": "Kaio", "age": 11, "email": "[email protected]" }, { "id": 3, "name": "Daniel", "age": 28, "email": "[email protected]" } ] } 

So I built a simple engine to handle this implementation:

const startEngine = (json) => (attributes) => ({ from: from(json, attributes) }); const buildAttributes = (node) => (acc, attribute) => ({ ...acc, [attribute]: node[attribute] }); const executeQuery = (attributes, attribute, value) => (resultList, node) => node[attribute] === value ? [...resultList, attributes.reduce(buildAttributes(node), {})] : resultList; const where = (json, attributes) => (attribute, value) => json .reduce(executeQuery(attributes, attribute, value), []); const from = (json, attributes) => (node) => ({ where: where(json[node], attributes) }); 

With this implementation, we can start the engine with the JSON data:

const select = startEngine(json); 

And use it like a SQL query:

select(['id', 'name']) .from('users') .where('id', 1); result; // [{ id: 1, name: 'TK' }] 

That's it for today. I could go on and on showing you a lot of different examples of abstractions, but I'll let you play with these concepts.

You can other articles like this on my blog.

My Twitter and Github.

Resources

  • Blog post source code
  • Closures | MDN Web Docs
  • Currying | Fun Fun Function
  • Learn React by building an App