Znak drogowy z białą strzałką na niebieskim tle, stojący przy ścianie pomalowanej kolorami tęczy.

Funkcje strzałkowe – Powtórka przed ReactJS #1

Na dobry początek, postanowiłem omówić jeden z najcieplej przyjętych dodatków, które zostały wprowadzone w specyfikacji ECMAScript 6. Gry robiłem pierwsze podejście do opanowania tych nowości, to właśnie funkcje strzałkowe jako pierwsze przyciągnęły moją uwagę. Jak się okazuję, nie byłem sam.

Funkcje strzałkowe - Wykres popularności funkcjonalności ES6

Źródło: Ankieta przeprowadzona na blogu 2ality.com.

Skąd taka popularność funkcji strzałkowych? Czy czas pożegnać się z starymi sposobami na definiowanie funkcji? Właśnie na następujące pytania postaram się odpowiedzieć w tym wpisie. Poruszę przy tym następujące zagadnienia:

  • Analiza składni
  • Leksykalne wiązanie this
  • Kiedy NIE używać funkcji strzałkowych

Na wytrwałych, którzy dobrną do końca wpisu, czeka lista zadań.

Analiza składni

Definicja funkcji strzałkowej składa się z listy parametrów, następującego po niej markera => oraz ciała funkcji.

() => { ... } // brak parametrów
x => { ... } // jeden parametr, identyfikator
(x, y) => { ... } // kilka parametrów

Nietrudno zauważyć, że w przypadku funkcji z jednym parametrem, nie pojawiły się przy nim nawiasy otaczające. Jest to jedno z udogodnień składniowych. Jednak, obowiązuje ono tylko, jeżeli parametr jest pojedynczym identyfikatorem. Cóż to oznacza w praktyce? Ano, jeżeli chcemy skorzystać z destrukturyzacji lub wartości domyślnej parametru, to musimy otoczyć go nawiasami.

// Funkcja z pojedynczym identyfikatorem, opuszczamy nawiasy otaczające parametr.
[1, 2, 3].map(x => 2 * x); // [2, 4, 6]
	
// Funkcja wykorzystująca destrukturyzacje, pamiętamy o użyciu nawiasów.
[[1,2], [3,4]].map(([a, b] => a + b); // [3, 7]
	
// Funkcja z parametrem o wartości domyślnej, nawiasy do boju.
const sayHello = (name="World") => console.log(`Hello ${name}`); 
sayHello(); // 'Hello World'

Przejdźmy do kolejnej zasadniczej różnicy, dzielącej funkcje strzałkowe od klasycznych deklaracji funkcji i wyrażeń funkcyjnych. Jak widać w powyższych przykładach, oprócz wspomnianego nawiasu, pozbyliśmy się również klamer oraz słowa kluczowego return. Klamry można pominąć w definicjach, których ciało zajmuje jedną linijkę. Konieczność jawnego użycia return jest zależna od zastosowania klamer.

// OK, brak bloku pozwala na wykorzystanie niejawnego return.
const sum = (a, b) => a + b;
// :(, każde wywołanie zwróci undefined, wewnątrz bloków wymagane jest jawne return.
const sum = (a, b) => { a + b 
// OK, jawne return.
const sum = (a, b) => { return a + b };

Im mniej linijek kodu, tym mniejsza szansa na wystąpienie błędu. To jedna z najczęściej wygłaszanych opinii przez miłośników czystego kodu. To niewątpliwie jeden z głównych powodów fenomenu funkcji strzałkowych. W większości przypadków pozwalają zrobić tyle samo, pisząc znacznie mniej. Sprawdźmy to na przykładzie. Powiedzmy, że chcemy podnieść do kwadratu wszystkie elementy tablicy, które są parzyste (everyday struggle).

const array = [1, 2, 3, 4, 5, 6];  

// Rozwiązanie z użyciem deklaracji funkcji.
function isEven(num) {
  return num % 2 === 0;
}

function square(num) {
  return num * num;
}

const squareOfEvens = array.filter(isEven).map(square);

// Rozwiązanie z wykorzystaniem funkcji strzałkowych.
const isEven = num => num % 2 === 0;
const square = num => num * num;

const squareOfEvens = array.filter(isEven2).map(square2);

Nie uwzględniając pustych linii, pierwsze rozwiązanie zajmuje ich 7, podczas gdy drugie zaledwie 3. Ponadto funkcje strzałkowe, w przypadku prostych wyrażeń, są dużo bardziej czytelne. Możemy skupić pełnię naszej uwagi na operacjach jakie wykonujemy, zamiast przedzierać się wzrokiem przez niepotrzebne klamry i słowa kluczowe.

Zobaczmy, jak sprawy się mają w przypadku czegoś bardziej wyszukanego. Weźmy na warsztat funkcję wykorzystującej domknięcia. Skorzystamy tutaj z klasycznego przykładu hodowania funkcji dodającej wybraną wartość do dowolnej liczby.

// Deklaracja przodem.
function makeAdder(x) {
  return function(y) {
    return x + y;
  }
}

// Do celu =>!
const makeAdder = x => y => x + y;

Po raz kolejny funkcja strzałkowa wyszła zwycięsko z pojedynku z poczciwą deklaracją. Jednak, nie zawsze ta zwięzłość składniowa działa na naszą korzyść. Przy bardziej skomplikowanych funkcjach warto rozważyć, który wariant jest bardziej zrozumiały i czytelny.

Wystarczy zachwytów związanych ze składnią funkcji strzałkowych. Na koniec wypadałoby wspomnieć o kilku szczegółach, na które warto uważać.

Marker => musi znaleźć się w tej samej linijce co lista parametrów, w innym przypadku silnik wyrzuci SyntaxError.

const sum = (a, b)  // Syntax error.
=> { return a + b; };

const sum = (a, b)  // Syntax error.
=> a + b;

const sum = (a, b) =>  // OK
{
  return a + b;
};

const sum = (a, b) => {  // OK
  return a + b;
};

const sum = (a, b) => // OK 
a + b;

Jeżeli ciało naszej funkcji zawiera instrukcję, niezależnie od ilości linijek kodu, musimy ją otoczyć klamrami.

asyncFunc.catch(x => { throw x });

Drugim specyficznym przypadkiem, jest funkcja zwracająca literał obiektowy. Aby wszystko poszło zgodnie z planem, musimy otoczyć go nawiasami. W tej sytuacji, zostaną one interpretowane jako operator grupujący dla zwracanego wyrażenia. Pozwala to wykorzystać omówiony wcześniej mechanizm niejawnego return. W przypadku pominięcia nawiasów, klucze naszego obiektu zostaną zainterpretowane jako etykiety, a wartości jako zwykłe łańcuchy.

// NOT OK, wywołanie zwróci undefined
const baz = () => { foo: 'bar' };
// OK
const baz = () => ({ foo: 'bar' });

Leksykalne this

Pewnie zastanawiacie się, skąd tyle zachodu z powodu zmiany, która jedynie wpływa na aspekt wizualny naszego kodu. Całe szczęście, główny bohater dzisiejszego wpisu niesie ze sobą dużo istotniejszą zmianę. Komitet TC39 uczynił z funkcji strzałkowych remedium na codzienne cierpienia programistów, którzy mieli dość wywoływania swoich funkcji z dopiskiem .bind(this) oraz korzystania z takich wytrychów jak var self = this.

Tradycyjne funkcje posiadają dynamiczne wiązanie this. W dużym skrócie, oznacza to, że wartość this jest zależna od kontekstu wywołania funkcji (temat rozwinę w nadchodzącym wpisie o tym wskaźniku). Taki mechanizm powoduje wiele utrudnień. Zwłaszcza gdy z jakiegoś powodu chcemy utworzyć obiekt dodający wybraną wartość, gdzie tylko język pozwoli (everyday struggle part 2).

function Adder(value) {
  this.value = value;
}

Adder.prototype.addToArray = function (arr) {
  'use strict';
  return arr.map(function (x) {
    return x + this.value;  // TypeError.
  });
}

Niestety, z takim kodem, nasz ambitny plan spali na panewce. W strict modecode>, wskaźnik this wewnątrz metody map przyjmie wartość undefined. Z pominięciem strict mode, odwoła się on do obiektu globalnego Windowcode>. W tym przypadku, nasza funkcja wypluje z siebie [NaN, NaN], a my zaczniemy się zastanawiać, czy programowanie w JSie, aby na pewno było dobrym pomysłem.

Oczywiście, programiści tworzą Addery na całym świecie. Geniusze na miarę Richarda Hendricksa szybko zorientowali się, że jest sposób na poprawne powiększenie elementów wybranej tablicy o ich ulubioną wartość. Wystarczy skorzystać jeden z wcześniej wspomnianych wspomagaczy.

// Wiążemy this z zakresu addToArray do funkcji mapującej za pomocą bind().
Adder.prototype.addToArray = function (arr) {
  return arr.map(function (x) {
    return x + this.value;
  }.bind(this));
}

// Korzystamy ze zmiennej pomocniczej self, w której zapisujemy wartość wskaźnika this. 
Adder.prototype.addToArray = function (arr) {
  let self = this;
  return arr.map(function (x) {
    return x + self.value;
  });
}

Zasadniczym minusem tych rozwiązań, zwłaszcza jeżeli nasza partnerka jest programistką, jest utrata elegancji naszego kodu. Kilka takich funkcji i usłyszymy, że nie robimy tego tak dobrze jak kiedyś. Mimo że nie mam takiego problemu, to dbam o los kolegów z branży. Mam do zaoferowania znacznie lepsze rozwiązanie.

// Do celu =>! (if you know what I mean) 
Adder.prototype.addToArray = function (arr) {
  return arr.map(x =>  {
		return x + this.value;
  });
}

Wskaźnik this przyjmuje wartość zgodną z oczekiwaniami, ponieważ funkcje strzałkowe posiadają zakres leksykalny. Jak działa zakres leksykalny? Po raz kolejny, w dużym skrócie, jeżeli identyfikator nie zostanie odnaleziony w aktywnym zakresie, jest on wyszukiwany w zakresie otaczającym.

Kiedy NIE używać funkcji strzałkowych

Zanim odjedziemy do stacji strzałkowego hype’u, warto zastanowić się, czy faktycznie mamy do czynienia z rozwiązaniem niezawodnym. Czyżby słowo kluczowe function wybierało się na emeryturę? Spoiler alert: nic bardziej mylnego.

Konstruktory

Wracając do naszego Addera, moglibyśmy pokusić się o przepisanie funkcji konstruującej z użyciem strzałki.

const Adder = (val) => {
  this.val = val;
}

const addTwo = new Adder(2); // TypeError: Adder is not a constructor

Czybyżby funkcje strzałkowe były wybrakowane w stosunku do swoich poprzedników? Na to wygląda. Zwykłe funkcje obsługują new za pomocą wewnętrznej metody [[Construct]] oraz właściwości prototype. Funkcje strzałkowe nie posiadają żadnej z nich, stąd zaobserwowany błąd.

Metody

Kolejny psikus czeka nas w przypadku metod. W tym przypadku, dokładnie ten sam mechanizm, który zmuszał nas do stosowania .bind(this), jest nam na rękę.

const algoSmart = {
  likes: 52,

	getLikes: () => console.log(this.likes); 
	setLikes: function(value) {
	  this.likes = value;
	}
};

algoSmart.getLikes(); // undefined
algoSmart.setLikes(100000); 

Wskaźnik this w metodzie getLikes() wskazuje na obiekt globalny Window. Jest to spowodowane brakiem utworzenia własnego wiązania this i wyszukiwaniem w zakresie leksykalnym. Nic straconego, sukces na social-media i tak mam zagwarantowany. Funkcja ustawiająca ilość lajków działa jak należy ;).

Obsługa zdarzeń

Analogicznie do metod, sytuacja powtarza się przy nasłuchiwaniu zdarzeń na elementach DOM.

<button id="example">Click me</button>

.on {
  background-color: #0288d1;
}

button = document.querySelector('#example');

button.addEventListener('click', () => {
  console.log(this); // Window!
  this.classList.toggle('on');
});

Funkcje wykorzystujące obiekt arguments

Tablico-podobny obiekt arguments przechowuje wszystkie argumenty przekazane do zadeklarowanej funkcji lub wyrażenia funkcyjnego. Niestety, na próżno szukać go wewnątrz funkcji strzałkowej. Funkcje strzałkowe nie wiążą własnego this, arguments, super oraz new.target. Jeżeli chcemy w naszej funkcji strzałkowej odwoływać się do wszystkich przekazanych argumentów, musimy skorzystać z spread operatora.

Jak żyć?

Miało być tak pięknie, wyszło jak zwykle. Biorąc pod uwagę wymienione wyjątki, kiedy można ze spokojem korzystać z funkcji strzałkowych? Proponuję tutaj podejście zaproponowane przez Larsa Schöninga na stackoverflow:

  1. Wykorzystuj function w zakresie globalnym oraz dla właściwości Object.prototype.
  2. Wykorzystuj class dla konstruktorów obiektów.
  3. Wykorzystuj => w każdym innym przypadku.

Jeżeli takie podejście do Ciebie nie przemawia, możesz zawsze skorzystać z niezawodnego, lecz w moich oczach karykaturalnego wykresu, zaproponowanego przez Kyle’a Simposona.

Zadania praktyczne

Wystarczy teorii na dziś, czas zrobić użytek z nabytej wiedzy. Przygotowałem dla Was listę linków do zadań, które warto wykonać:

  1. ES6 Katas (2 zadania)
  2. ES6 Sandbox (3 zadania)
  3. Javascript.info (1 zadanie)

Każdy z odnośników prowadzi do edytora online z treścią zadania i przygotowanym środowiskiem. Na pewno nie chcesz, aby czas poświęcony na przeczytanie tego wpisu poszedł na marne. Najlepszym sposobem na utrwalanie wiedzy jest praktyka.

Podsumowanie

Mam nadzieję, że udało mi się poszerzyć Waszą wiedzę dotyczącą funkcji strzałkowych. Jest to świetna funkcjonalność, ale trzeba wiedzieć, kiedy należy ją wykorzystywać. W razie wątpliwości, zachęcam do zadawania pytań w komentarzach oraz za pomocą formularza kontaktowego.

Zachęcam do śledzenia bloga na facebooku. Ponadto jestem dumnym posiadaczem lekko zakurzonego Twittera i raczkującego profilu na LinkedIn.

W następnym wpisie omówię kolejną funkcjonalność wprowadzoną w ES6. Będą to łańcuchy szablonowe. Do zobaczenia w przyszły poniedziałek.

Źródła

Zdjęcie tytułowe autorstwa:

Dmitri Popov