Bezárások, Curried függvények és jó absztrakciók a JavaScript-ben

Ebben a cikkben a bezárásokról és a curried funkciókról fogunk beszélni, és ezen fogalmakkal fogunk játszani, hogy hűvös absztrakciókat készítsünk. Szeretném bemutatni az egyes koncepciók ötletét, de nagyon praktikusvá is tenni példákkal és átdolgozott kódokkal, hogy szórakoztatóbbá tegyük őket.

Zárások

A bezárások a JavaScript egyik általános témája, és ezzel kezdjük. Az MDN szerint:

A bezárás egy függvény kombinációja (mellékelve) a környező állapotára (a lexikális környezetre) való hivatkozásokkal.

Alapvetően minden egyes funkció létrehozásakor létrejön egy bezárás is, amely hozzáférést biztosít az állapothoz (változók, konstansok, függvények stb.). A környező állam a lexical environment.

Mutassunk egy egyszerű példát:

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

Mi van itt nekünk?

  • Fő funkciónkat hívjuk makeFunction
  • A namekarakterlánccal egy állandó nevet kapunk ,'TK'
  • A displayNamefüggvény meghatározása (amely csak az nameállandót naplózza )
  • És végül makeFunctionvisszaadja a displayNamefüggvényt

Ez csak egy függvény meghatározása. Ha hívjuk makeFunction, akkor mindent létrehoz benne: állandó és egy másik függvényt, ebben az esetben.

Mint tudjuk, a displayNamefüggvény létrehozásakor létrejön a bezárás is, és a funkciót tudatosítja környezetében, jelen esetben az nameállandóban. Ezért lehet console.logaz nameállandó törés nélkül semmit. A függvény ismeri a lexikális környezetet.

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

Nagy! A várakozásoknak megfelelően működik. A visszatérő értéke makeFunctionegy függvény, amelyet az myFunctionállandóban tárolunk . Amikor felhívunk myFunction, megjelenik TK.

Nyílfunkcióként is működtethetjük:

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

De mi van, ha át akarjuk adni a nevet és megjelenítjük? Egyszerű! Használjon paramétert:

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

Most játszhatunk a névvel:

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

myFunction tisztában van az átadott argumentummal, és arról, hogy ez alapértelmezett vagy dinamikus érték.

A bezárás biztosítja, hogy a létrehozott függvény ne csak az állandókat / változókat ismerje, hanem a függvényen belüli egyéb funkciókat is.

Tehát ez is működik:

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

A visszaküldött függvény ismeri a displayfüggvényt és képes meghívni.

Az egyik hatékony technika a bezárások használata a "privát" funkciók és változók felépítéséhez.

Hónapokkal ezelőtt (újra!) Adatstruktúrákat tanultam, és mindegyiket szerettem volna megvalósítani. De mindig az objektumorientált megközelítést használtam. Funkcionális programozási rajongóként az összes adatstruktúrát szerettem volna felépíteni az FP elvek szerint (tiszta funkciók, változtathatatlanság, referencia átláthatóság stb.).

Az első adatstruktúra, amelyet megtanultam, a Stack volt. Nagyon egyszerű. A fő API:

  • push: tegyen egy elemet a verem első helyére
  • pop: távolítsa el az első elemet a veremből
  • peek: az első elem megszerzése a veremből
  • isEmpty: ellenőrizze, hogy a verem üres-e
  • size: megkapja a veremben lévő elemek számát

Egyértelműen létrehozhatunk egy egyszerű függvényt az egyes "módszerekhez", és továbbíthatjuk a verem adatait. Ezután felhasználhatja / átalakíthatja az adatokat és visszaadhatja azokat.

Hozhatunk létre privát adatokkal rendelkező veremeket is, és csak az API módszereket tárhatjuk fel. Csináljuk!

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, }; }; 

Mivel mi itemsa buildStackfüggvényünkön belül hoztuk létre a verem , ez "privát". Csak a funkción belül érhető el. Ebben az esetben csak push,, popés így lehet megérinteni az adatokat. Pontosan ezt keressük.

És hogyan használjuk? Mint ez:

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 

Tehát a verem létrehozásakor az összes funkció ismeri az itemsadatokat. De a funkción kívül nem férhetünk hozzá ezekhez az adatokhoz. Ez privát. Csak módosítjuk az adatokat a verem beépített API-jával.

curry

"A curry az a folyamat, amikor egy függvényt több argumentummal veszünk fel, és függvények sorozatává alakítjuk, amelyek mindegyikének csak egyetlen argumentuma van."

- Frontend interjú

Így elképzelhető, hogy van egy funkció több paraméterek: f(a, b, c). A curry használatával olyan függvényt érünk el, amely egy olyan függvényt f(a)ad vissza, amely egy függvényt g(b)ad vissza h(c).

Alapvetően: f(a, b, c)->f(a) => g(b) => h(c)

Készítsünk egy egyszerű példát, amely két számot ad hozzá. De először, curry nélkül:

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

Nagy! Szuper egyszerű! Itt van egy két argumentumú függvény. Ahhoz, hogy átalakítsuk curried függvényt, szükségünk van egy olyan függvényre, amely fogad xés visszaad függvényt, amely ymindkét érték összegét megkapja és visszaadja.

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

Átalakíthatunk addYegy névtelen nyílfüggvényt :

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

Vagy egyszerűsítse úgy, hogy felépít egy bélésnyíl funkciót:

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

Ennek a három különböző curried függvénynek ugyanaz a viselkedése: csak egy argumentummal hozza létre a függvények sorozatát.

Hogyan tudjuk használni?

add(10)(20); // 30 

Eleinte kissé furcsának tűnhet, de logika áll mögötte. add(10)függvényt ad vissza. És ezt a függvényt hívjuk az 20értékkel.

Ez ugyanaz, mint:

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

És ez érdekes. Speciális függvényeket generálhatunk az első függvény meghívásával. Képzelje el, hogy egy incrementfunkciót akarunk . Funkciónkból generálhatjuk, ha értékként addadjuk 1meg.

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

Amikor a Lazy Cypress-et, egy npm könyvtárat implementáltam a felhasználói viselkedés rögzítésére egy űrlapoldalon és a Cypress tesztkód generálásához, fel akartam építeni egy függvényt ennek a karakterláncnak a létrehozására input[data-testid="123"]. Tehát megvolt az elem ( input), az attribútum ( data-testid) és az érték ( 123). Interpoláló ez a karakterlánc JavaScript nézne ki: ${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