ES6 bez tajemnic. Funkcje strzałkowe

01 grudnia 2015
1 gwiadka2 gwiazdki3 gwiazdki4 gwiazdki5 gwiazdek

ES6 bez tajemnic to cykl artykułów poświęconych nowym składnikom języka JavaScript, które pojawiły się w 6. edycji standardu ECMAScript, w skrócie ES6.

Strzałki istnieją w JavaScripcie od zawsze. W pierwszych poradnikach dotyczących tego języka zalecano, by umieszczać skrypty znajdujące się bezpośrednio w kodzie w komentarzach HTML. Dzięki temu w przeglądarkach nieobsługujących JS treść skryptu nie byłaby błędnie wyświetlana jako tekst. Pisano więc kod w stylu:

<script language="javascript">
<!--
  document.bgColor = "brown";  // czerwony
// -->
</script>

Starsze przeglądarki widziały jedynie dwa nieobsługiwane znaczniki i komentarz, a tylko nowe wykrywały kod JS.

Aby można było zastosować tę dziwną sztuczkę, silnik JavaScriptu w przeglądarkach traktuje znaki <!-- jako początek jednowierszowego komentarza. Poważnie. Oznaczenie to funkcjonuje od samego początku istnienia JavaScriptu aż po dziś dzień, i to nie tylko bezpośrednio przed znajdującym się w kodzie znacznikiem script, lecz także w dowolnym miejscu w kodzie JS. Nawet w środowisku Node.

Tak się składa, że ten styl zapisywania komentarzy zostanie po raz pierwszy ustandaryzowany w ES6. Ale nie o takiej strzałce chcemy dziś powiedzieć.

Także przypominająca strzałkę sekwencja --> oznacza komentarz jednowierszowy. Co dziwne, podczas gdy w języku HTML znaki znajdujące się przed –> stanowią część komentarza, w JS komentarz znajduje się po sekwencji –>.

Jest jeszcze coś. Strzałka oznacza komentarz tylko jeśli umieszczona jest na początku wiersza — w innych kontekstach sekwencja –> to operator JS, tak zwany operator odliczania (ang. "goes to" operator)!

function countdown(n) {
  while (n --> 0)  // odliczanie od n do zera
    alert(n);
  blastoff();
}

Ten kod naprawdę działa. Pętla jest wykonywana do czasu, gdy n osiągnie wartość 0. Również i to nie jest nowym składnikiem w ES6, lecz nieco dezorientującym połączeniem kilku znanych już elementów. Potrafisz rozgryźć co tu się dzieje? Jak zwykle, odpowiedź można znaleźć w portalu Stack Overflow.

Oczywiście istnieje także operator <= (mniejszy lub równy). Z kodu JS dałoby się pewnie wyłowić jeszcze więcej strzałek, ale skupmy się teraz na strzałce, której brakuje.

<!--komentarz jednowierszowy
-->operator odliczania
<=mniejszy lub równy
=>???

Co się stało ze strzałką =>? Tego dowiesz się w dalszej części artykułu, a na początek pomówmy nieco o funkcjach JavaScript.

Wyrażenia funkcyjne są wszędzie

Fajną właściwością JavaScriptu jest to, że zawsze gdy potrzebujesz jakiejś funkcji możesz ją po prostu napisać w środku bieżącego kodu.

Załóżmy na przykład, że chcesz przekazać przeglądarce co ma zrobić, kiedy użytkownik kliknie określony przycisk. Zaczynasz więc pisać:

$("#confetti-btn").click(

Metoda .click() z jQuery przyjmuje jeden argument: funkcję. Żaden kłopot. Funkcję można napisać od razu w tym miejscu:

$("#confetti-btn").click(function (event) {
  playTrumpet();
  fireConfettiCannon();
});

Pisanie kodu w taki sposób przychodzi nam już naturalnie. Teraz więc może wydawać się to dziwne, że nim JavaScript spopularyzował taki styl programowania, w wielu językach ten element funkcjonalności był niedostępny. Oczywiście w języku Lisp już w 1958 r. pojawiły się wyrażenia funkcyjne, tak zwane funkcje lambda, jednak w C++, Pythonie, C# czy Javie przez lata nie było czegoś podobnego.

To już jednak historia. Dzisiaj z lambd można korzystać we wszystkich tych językach, zaś w nowych językach lambdy są zawsze wbudowane. Jest to zasługą JavaScriptu – i jego pierwszych programistów, którzy nieustraszenie tworzyli biblioteki bazujące w dużej mierze na lambdach, co doprowadziło do rozpowszechnienia tych funkcji.

Smutne jest więc, że ze wszystkich wymienionych przeze mnie języków to w JavaScripcie składnia lambd jest najbardziej rozwlekła.

// bardzo prosta funkcja w sześciu językach
function (a) { return a > 0; } // JS
[](int a) { return a > 0; }  // C++
(lambda (a) (> a 0))  ;; Lisp
lambda a: a > 0  # Python
a => a > 0  // C#
a -> a > 0  // Java

Nowa strzałka do kolekcji

W ES6 pojawiła się nowa składnia funkcji.

// ES5
var selected = allJobs.filter(function (job) {
  return job.isSelected();
});

// ES6
var selected = allJobs.filter(job => job.isSelected());

Jeśli potrzebujemy prostej funkcji z jednym argumentem, nowa funkcja strzałkowa przybierze po prostu postać identyfikator => wyrażenie. Nie trzeba pisać słowa kluczowego function ani return, opuszczamy także nawiasy, klamry i średnik.

(Osobiście bardzo cieszę się z tej nowości. Z mojego punktu widzenia istotne jest, że nie trzeba pisać słowa function – za każdym razem piszę functoin, przez co muszę się wracać i poprawiać literówkę).

Aby natomiast napisać funkcję z wieloma argumentami (albo z żadnymi, bądź też z parametrami resztowymi, domyślnymi lub z argumentem destrukturyzującym), listę argumentów należy umieścić w nawiasie okrągłym.

// ES5
var total = values.reduce(function (a, b) {
  return a + b;
}, 0);

// ES6
var total = values.reduce((a, b) => a + b, 0);

Moim zdaniem prezentuje się to całkiem dobrze.

Funkcje strzałkowe działają równie dobrze z narzędziami funkcyjnymi dostępnymi w bibliotekach, np. Underscore.js czy Immutable. Co więcej, wszystkie przykłady zwarte w dokumentacji Immutable są napisane zgodnie ze standardem ES6, dlatego wiele z nich wykorzystuje funkcje strzałkowe.

A co jeśli chodzi o inne zastosowania? Funkcje strzałkowe mogą zawierać cały blok instrukcji zamiast pojedynczego wyrażenia. Przypomnijmy sobie nasz poprzedni przykład:

// ES5
$("#confetti-btn").click(function (event) {
  playTrumpet();
  fireConfettiCannon();
});

Tak będzie on wyglądać w ES6:

// ES6
$("#confetti-btn").click(event => {
  playTrumpet();
  fireConfettiCannon();
});

Jest to niewielkie usprawnienie. Większe zmiany moglibyśmy zaobserwować z wykorzystaniem obietnic (ang. promise), ponieważ fragment }).then(function (result) { może się nawarstwiać.

Warto zwrócić uwagę, że funkcja strzałkowa zawierająca blok kodu nie zwraca automatycznie wartości. W tym celu należy wykorzystać instrukcję return.

Przy tworzeniu prostych obiektów za pomocą funkcji strzałkowych trzeba pamiętać o jednej rzeczy – obiekt zawsze musi zostać umieszczony w nawiasie okrągłym:

// utwórz nowy pusty obiekt do zabawy dla każdego psiaka
var chewToys = puppies.map(puppy => {});   // BŁĄD!
var chewToys = puppies.map(puppy => ({})); // ok

Niestety pusty obiekt – {} – i pusty blok – {} – wyglądają identycznie. W ES6 obowiązuje zasada, że nawias klamrowy znajdujący się bezpośrednio po strzałce jest zawsze traktowany jako początek bloku, nigdy jako początek obiektu. Z tego powodu kod puppy => {} jest interpretowany jako funkcja strzałkowa, która nic nie robi i zwraca wartość undefined.

Co jeszcze bardziej mylące, literał obiektowy w postaci {klucz: wartość} wygląda tak samo jak blok zawierający instrukcję z etykietą — a przynajmniej tak postrzega go silnik JavaScriptu. Na szczęście nawias klamrowy to jedyny dwuznaczny znak, a opakowywanie literałów obiektowych w nawias okrągły to jedyna sztuczka, o jakiej musisz pamiętać.

Czym jest this

Istnieje jedna subtelna różnica między sposobem działania zwykłych funkcji definiowanych za pomocą słowa kluczowego function a sposobem działania funkcji strzałkowych. Funkcje strzałkowe nie mają własnej wartości this. Wartość znajdującego się w funkcji strzałkowej this jest zawsze dziedziczona z wyższego zakresu.

Zanim jednak sprawdzimy co to oznacza w praktyce, zatrzymajmy się na chwilę.

Jak obiekt this funkcjonuje w JavaScripcie? Skąd bierze się jego wartość? Na to pytanie nie ma krótkiej odpowiedzi. Jeśli wydaje ci się to proste, to dlatego, że miałeś z tym do czynienia już od dawna!

Pytanie to pojawia się tak często między innymi dlatego, że funkcje definiowane za pomocą słowa kluczowego function otrzymują wartość this automatycznie, czy tego chcą, czy nie. Czy kiedyś zdarzyło Ci się zastosować taką sztuczkę?

{
  ...
  addAll: function addAll(pieces) {
    var self = this;
    _.each(pieces, function (piece) {
      self.add(piece);
    });
  },
  ...
}

W funkcji wewnętrznej chciałoby się po prostu napisać this.add(piece) . Niestety funkcja wewnętrzna nie dziedziczy wartości this z funkcji zewnętrznej. W funkcji wewnętrznej this będzie mieć wartość window lub undefined. Zewnętrzną wartość this można jednak przemycić, korzystając z tymczasowej zmiennej self. (Innym rozwiązaniem jest wywołanie metody .bind(this) na funkcji wewnętrznej. Żaden z tych sposobów nie jest jednak szczególnie przyjemny dla oka).

W ES6 będziesz mógł zapomnieć o wszystkich tych sztuczkach, jeśli korzystając z this będziesz kierować się poniższymi zasadami:

  • Z funkcji „niestrzałkowych” należy korzystać w przypadku metod wywoływanych poprzez składnię obiekt.metoda(). Funkcje te otrzymają sensowną wartość this od wywołującego je obiektu.
  • Funkcji strzałkowych należy używać w pozostałych przypadkach.
// ES6
{
  ...
  addAll: function addAll(pieces) {
    _.each(pieces, piece => this.add(piece));
  },
  ...
}

Zauważ, że w ES6 metoda addAll otrzymuje wartość this od wywołującego ją obiektu. Funkcja wewnętrzna jest funkcją strzałkową, zatem dziedziczy this z wyższego zakresu.

Dodatkowo ES6 umożliwia pisanie metod w literałach obiektowych w krótszy sposób! Powyższy kod można więc jeszcze bardziej uprościć:

// składnia metod w ES6
{
  ...
  addAll(pieces) {
    _.each(pieces, piece => this.add(piece));
  },
  ...
}

Metody, strzałki… Być może już nigdy nie zdarzy mi się napisać słowa functoin. To miła perspektywa.

Istnieje jeszcze jedna drobna różnica pomiędzy zwykłymi funkcjami a funkcjami strzałkowymi: funkcje strzałkowe nie otrzymują również własnego obiektu arguments. W ES6 pewnie jednak i tak skorzystamy raczej z parametru resztowego bądź domyślnego.

Strzałki – narzędzie pomagające zgłębić podstawy informatyki

Omówiliśmy wiele praktycznych zastosowań funkcji strzałkowych. Chciałbym przedstawić jeszcze jedno: funkcje strzałkowe z ES6 można wykorzystać jako narzędzie edukacyjne, by odkryć coś dotyczącego mrocznej natury obliczeń. Czy jest to praktyczne, czy nie – o tym będziesz musiał już sam zdecydować.

W 1936 r. Alonzo Church i Alan Turing rozwijali swoje niezależne, potężne matematyczne modele obliczeniowe. Turing nadał swojemu modelowi nazwę a-machines, jednak wszyscy zaczęli nazywać go maszyną Turinga. Church natomiast pisał o funkcjach. Jego model nosił nazwę rachunek λ (λ to mała litera lambda w alfabecie greckim). Od niego też wywodzi się nazwa określająca funkcje w języku Lisp – LAMBDA, i dlatego dziś lambdami nazywamy wyrażenia funkcyjne.

Ale czym jest rachunek lambda? I czym jest „model obliczeniowy”?

Trudno ująć to w kilku słowach, jednak postaram się to krótko wyjaśnić: rachunek lambda to jeden z pierwszych języków programowania. Nie został on zaprojektowany jako język programowania – w końcu komputery przechowujące programy pojawiły się dopiero co najmniej dekadę później. Było to raczej bardzo proste, czysto matematyczne wyobrażenie języka, w którym można by wyrazić dowolne obliczenie. Churchowi zależało, by za pomocą tego modelu można było udowodnić ogólne prawa rządzące obliczeniami.

Odkrył, że w jego systemie brakuje jednej rzeczy: funkcji.

Zastanów się, jakież to niezwykłe stwierdzenie: każde obliczenie wykonywane przez JavaScript można wyrazić jedynie za pomocą funkcji. Bez obiektów, bez tablic, bez liczb, bez instrukcji warunkowych if, pętli while, średników, przypisania, operatorów logicznych czy nawet pętli zdarzeń.

Oto przykład tego typu „programu” matematycznego wykorzystującego notację lambda Churcha:


fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))

Równoważna funkcja JavaScript wygląda zaś następująco:

var fix = f => (x => f(v => x(x)(v)))
               (x => f(v => x(x)(v)));

Innymi słowy, JavaScript zawiera działającą implementację rachunku lambda. Rachunek lambda jest zawarty w JavaScripcie.

Opowieści o tym, jak Alonzo Church i późniejsi badacze wykorzystywali rachunek lambda i jak wkradł się on dyskretnie do niemalże każdego głównego języka programowania wykraczają już poza zakres tego artykułu. Jeśli jednak interesują cię podstawy informatyki albo chciałbyś zobaczyć jak język oparty wyłącznie na funkcjach radzi sobie z odwzorowywaniem zachowania pętli czy rekurencji, to warto spędzić deszczowe popołudnie na zgłębianiu liczb naturalnych Churcha i operatorów paradoksalnych. Możesz pobawić się nimi w konsoli przeglądarki Firefox lub w brudnopisie. JavaScript, wraz ze strzałkami ES6 stanowiącymi jeden z jego mocniejszych punktów, można śmiało uznać za najlepszy język do poznawania rachunku lambda.

Kiedy będę mógł zacząć korzystać z funkcji strzałkowych?

Funkcje strzałkowe ES6 zostały zaimplementowane w Firefoksie przeze mnie w 2013 r. i stały się szybsze dzięki Janowi de Mooijowi. Podziękowania dla Tooru Fujisawy i ziyunfei za łatki.

Funkcje strzałkowe zostały również zaimplementowane w wersji podglądowej przeglądarki Microsoft Edge. Ponadto można z nich korzystać w kompilatorach Babel i Traceur, a także w języku TypeScript – jeśli więc jesteś zainteresowany, to możesz zacząć używać funkcji strzałkowych w kodzie przeglądarkowym już dziś.

Tematem kolejnego artykułu będzie jeden z dziwniejszych składników ES6. Zobaczymy, że operator typeof zwraca zupełnie nową wartość. Zadamy pytanie: kiedy nazwa nie jest łańcuchem? Zastanowimy się także nad tym, co oznacza równość. Przygotuj się zatem na coś dziwnego i odwiedzaj nas często, by szczegółowo poznać symbole w ES6.

Autor: Jason Orendorff

Źródło: https://hacks.mozilla.org/2015/06/es6-in-depth-arrow-functions/

Tłumaczenie: Joanna Liana

Treść tej strony dostępna jest na zasadach licencji CC BY-SA 3.0

1 komentarz

  1. „nowa funkcja strzałkowa przybierze po prostu postać identyfikator => wyrażenie”

    Czy nie lepiej byłoby napisać argument(y) => wyrażenie?

    „Funkcji strzałkowych należy używać w pozostałych przypadkach.”

    Nie zgadzam się. Funkcje strzałkowe są zawsze anonimowe. Skoro tak to spada czytelność i samodokumentacja kodu. I jeszcze pozostaje kwestia stosu wywołań.
    To czy należy stosować funkcje strzałkowe czy nie jest bardziej skomplikowane. Dobrze opisał to Kyle Simpson w swej serii YDKJS

    Odpowiedz

Dyskusja

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *