poniedziałek, 13 listopada 2017

Domknięcie (closure), co to jest i z czym to się je?

Na stronie MDN jest taka definicja A closure is the combination of a function and the lexical environment within which that function was declared. Pamiętam, jak przeczytałam ją pierwszy raz. Jak to mawiała moja mama "mózg mi stanął w poprzek". Każde słowo z osobna wydawało mi się co najmniej znajome, nawet jeśli nie w 100% zrozumiałe. Ale wszystkie do kupy powodowały, że mi się odechciewało.

To było wiele lat temu. Wiele lat, w trakcie których wciąż od nowa odkrywałam definicję domknięcia. Wczoraj robiłam proste zadanko, podpięcie pod kolumnę z danymi numerycznymi sortowania po liczbach a nie alfabetycznie (żeby 70 było mniejsze niż 320). Metodę napisałam dość banalną:

function sort(a, b, order) {
  return order === 'desc' ?
    parseFloat(a.balance) - parseFloat(b.balance) :
    parseFloat(b.balance) - parseFloat(a.balance);
};

Zadowolona z wyniku wzięłam się za testowanie. Już po chwili odkryłam, że poza kolumną opisaną w zadaniu, w tabeli jest więcej sortowalnych kolumn z danymi numerycznymi. Moja metoda, która świetnie spełniała swoje zadanie, dla jednej konkretnej kolumny, nie będzie działać, dla dowolnej innej kolumny, ponieważ sortuję wartości ukryte we właściwości balance (a.balance i b.balance). Dla innych kolumn potrzebowałam bardziej elastycznego rozwiązania.

Najłatwiej byłoby dodać nazwę kolumny, do argumentów funkcji. Ale interface funkcji przewidywał 3 konkretne argumenty: pierwszą wartość do sortowania, drugą wartość do sortowania oraz kierunek sortowania. I tu właśnie na pomoc przyszło mi domknięcie.

Ponieważ nie mogę przekazać do mojej funkcji nazwy właściwości, która przechowuje wartość do sortowania, muszę sprawić, żeby moja funkcja została stworzona w środowisku, które już zna tę nazwę. Ten przykład jasno pokazuje o co mi chodzi:

var name = 'Ania';

function greeting() {
  console.log('hello ' + name);
}

Wewnątrz funkcji greeting użyta jest zmienna name utworzona na zewnątrz funkcji. Tego typu działanie nazywa się scope, wszystko to co jest stworzone na zewnątrz funkcji jest widoczne wewnątrz, ale jeśli coś zostało stworzone wewnątrz (z użyciem któregoś ze słów kluczowych var, let lub const) nie jest widoczne na zewnątrz.

Czyli potrzebuję stworzyć środowisko, w którym znana będzie nazwa właściwości która przechowuje wartość do sortowania. Do tego środowiska będę przekazywać tę nazwę (w każdej kolumnie będę korzystać z innej właściwości):

function sortByPropName(propName) {}

Środowisko już jest, wnętrze funkcji będzie znało wartość przekazanego tam argumentu propName, jednak ja do tabeli mam przekazać funkcję, która bedzie przyjmować 3 konkretne argumenty.

function sortByPropName(propName) {
  return function(a, b, order) {}
}

Coś zaczyna z tego wychodzić. Mam już funkcję z potrzebnymi danymi, która zwraca inną funkcję, która oczekuje argumenty wyspecyfikowane w interfejsie. Teraz wystarczy dodać samą logikę, która uwzględnia przekazaną nazwę właściwości przechowujacej wartość do sortowania:

function sortByPropName(propName) {
  return function(a, b, order) {
    return order === 'desc' ?
      parseFloat(a[propName]) - parseFloat(b[propName]) :
      parseFloat(b[propName]) - parseFloat(a[propName]);
  }
}

Wygląda nieźle, prawda? To teraz wystarczy przekazać wynik sortByPropName jako funkcję sortujacą dla kolumny (kod jsx):

<TableHeaderColumn dataField="balance" dataFormat={signedNumber} dataSort sortFunc={sortByPropName('balance')}>Balance</TableHeaderColumn>
<TableHeaderColumn dataField="total_amount" dataFormat={signedNumber} dataSort sortFunc={sortByNumericValue('total_amount')}>Total amount</TableHeaderColumn>

Powyższy kod używa HTML wewnątrz JS, ale nie jak ciągu tekstowego, a bardziej jak obiektów XML. To co jest najważniejsze, to sortFunc, do której przekazuję WYNIK działania sortByPropName. sortFunc oczekuje na funkcję sortującą, która będzie przyjmowała 3 argumenty, czyli dokładnie to, co zwraca sortByPropName.

A gdzie w tym wszystkim jest domknięcie?

Dokładnie w tym, co zwraca sortByPropName. Wartość argumentu propName jest znana tylko wewnątrz funkcji, gdzie jest tworzona kolejna funkcja, która z tej wartości korzysta. Tę funkcję zwracamy na zewnątrz, gdzie jest przypisana do nazwy sortFunc i wykorzystana wewnątrz komponentu TableHeaderColumn. Pomimo, że sortFunc jest używana w zupełnie innym środowisku, ma dostęp do zmiennych ze środowiska, w którym została stworzona. To jest właśnie domknięcie.