Funkcja strzałkowa

Do tej pory stosowaliśmy dwie metody definiowania funkcji:


//wyrażenie
const myF = function(a, b) {
    return a + b
}

//deklaracja
function myF(a, b) {
    return a + b
}

ES6 wprowadza nowy sposób deklarowania funkcji zwący się funkcją strzałkową (lub też grubą strzałką, czyli fat arrow).

Funkcja strzałkowa ma dwie charakterystyczne cechy. Skraca zapis funkcji, oraz zmienia kontekst this.

Skrócony zapis

Przypuśćmy, że mamy prostą deklarację funkcji:


const myF = function(a, b) {
    return a + b
}

Teraz chcielibyśmy tą samą funkcję zadeklarować za pomocą funkcji strzałkowej:


const myF = (a, b) => { return a + b }

Aż tak wiele nam nie uprościło. Zamiast słowa function pojawiła się charakterystyczna "gruba strzałka" (fat arrow).
No to upraszczamy dalej.

Jeżeli funkcja wymaga tylko jednego parametru, wtedy mogę pominąć nawiasy:


const myF = function(a) { return a * a }

const myF = a => { return a * a }

Jeżeli funkcja ma tylko jedną instrukcję (powyżej tylko ta z return) to mogę pominąć klamry:


const myF = function(a) { console.log( a * a ) }

const myF = a => console.log( a * a );

Jeżeli jedyną instrukcją funkcji jest ta zwracająca, możemy pominąć słowo return:


const myF = function(a) { return a * a }

const myF = a => a * a

Gdy funkcja nie ma atrybutów, lub ma ich więcej, nawiasy muszę podawać:


const myF = function(a, b) { return a * b }
const myF = (a, b) => a * b

const myF = function() { return "ala ma kota" }
const myF = () => "ala ma kota"

Jeżeli funkcja ma więcej instrukcji, wtedy muszę użyć klamer:


const myF = function(a, b) {
    const ret = a * b;
    console.log( ret );
    return ret;
}

const myF = (a, b) => {
    const ret = a * b;
    console.log( ret );
    return ret;
}

const myF = function() {
    const text = " ma kota";
    console.log(result);
    return "ala" + text;
}

const myF = () => {
    const text = " ma kota";
    console.log(result);
    return "ala" + text;
}

const myF2 = a => {
    const result = "Wynik: " + (a*a);
    console.log(result);
    return result;
}

Jeżeli jedyną instrukcją jest zwracanie literału obiektu, wtedy zachodzi konflikt między redukcją klamer (co powyżej), a klamrami obiektu. W takim przypadku zwracany obiekt trzeba objąć nawiasami:


const returnObj = name => ({ team : name, score : 0 })

Przykłady użyć

Poza skróceniem zapisu wyrażeń funkcyjnych, najczęściej funkcje strzałkowe pojawiają się tam, gdzie używaliśmy funkcji anonimowych czyli przy wszelakich eventach, sortach, forEach, fetch itp.

W rozdziale o tablicach na samym dole pojawiło się kilka specyficznych funkcji do iteracji.

Funkcja strzałkowa bardzo upraszcza ich zapis:


const tabUsers = [
    { name : 'Marcin', age: 18 },
    { name : 'Ania', age: 16 },
    { name : 'Agnieszka', age: 16}
];
const tabNr = [1, 2, 3];

//wypisujemy nazwy użytkowników wielkimi literami

//poprzednio
tabUsers.forEach(function(el) {
    console.log(el.name.toUpperCase());
});

//teraz
tabUsers.forEach(el => console.log(el.name.toUpperCase()));

//Tworzymy nową tablicę z liczbami 2x większymi

//poprzednio
const tab2 = tabNr.map(function(el) {
    return el * 2;
});

//teraz
const tab2 = tabNr.map(el => el * 2);

//sprawdzamy czy wszyscy użytkownicy są pełnoletni

//poprzednio
const allMinors = tabUsers.every(function(el) {
    return el.age > 18;
});

//teraz
const allMinors = tabUsers.every(el => el.age > 18);

//sprawdzamy czy niektórzy użytkownicy są pełnoletni

//poprzednio
const isSomeOfAge = tabUsers.some(function(el) {
    return el.age > 18
});

//teraz
const isSomeOfAge = tabUsers.some(el => el.age > 18);

//sprawdzam czy ostatnia litera imienia to "a" (nędzne założenie że to kobiece imię, co jest nieprawdą...)

//poprzednio
const woman = tabUsers.filter(function(el) {
    return el.charAt(el.length-1) === 'a';
});

//teraz
const woman = tabUsers.filter(el => el.charAt(el.length-1) === 'a');

Dzięki takim uproszczeniom bardzo fajnie można łączyć dane konstrukcje we wspólne całości:


if ( tabUsers.some(el => el.age > 18) ) {
    console.log("W tabeli są użytkownicy pełnoletni");
}


const woman = tabUsers
                .map(el => el.name.toUpperCase())
                .filter(el => el.charAt(el.length-1) === 'A')
                .join(" i ");

console.log(namesOfWoman); //"ANIA i AGNIESZKA"

Podobne uproszczenia możemy stosowac przy innych sytuacjach np. jQuerowym Ajaxie czy fetchu:


$.ajax({...})
.done(function(result) {
    console.log(result);
})
.fail(function(error) {
    alert("wystąpił błąd");
});

//teraz
$.ajax({...})
.done(result => console.log(result))
.fail(error => alert(error));

W ramach prostego ćwiczenia spróbuj odmienić poniższy zapis na klasyczne funkcje:


fetch('https://jsonplaceholder.typicode.com/todos/1')
    .then(response => response.json())
    .then(json => console.log(json))

Zmiana kontekstu dla this

Powyższe skracanie zapisu to jednak detale. Najwyżej będziemy stosować dłuższy zapis.

Najważniejszą sprawą w funkcji strzałkowej jest to, że w odróżnieniu od klasycznych funkcj nie tworzy ona wiązania this.

Jak pamiętasz z rozdziału o obiektach wewnątrz funkcji zmienna this zazwyczaj wskazuje na obiekt, który daną metodę wywołał (czyli znajduje się przed kropką np. ob.print()):


const ob = {
    name : "Batman",
    printName() { //następuje wiązanie this z obiektem ob
        console.log(this.name);
    }
}

ob.printName(); //this w printName wskaże na ob

W przypadku funkcji strzałkowych takie wiązanie this nie zachodzi. Oznacza to, że wewnątrz danej funkcji strzałkowej this ma taką samą wartość jak w otaczającym go środowisku. Osobiście lubię tłumaczyć, że "this brane jest z zewnątrz".


const ob = {
    name : "Batman",
    printName : () => { //nie zachodzi wiązanie - this === window
        console.log(this.name);
    }
}

ob.printName();

W powyższym przykładzie nie ma to totalnie sensu, ale już na przykład w funkcjach zagnieżdżonych jest to całkiem fajnym rozwiązaniem:


const ob = {
    name : "Batman",

    btn() {
        const btn = document.querySelector('.btn');
        btn.addEventListener('click', () => {
            //normalnie this wskazywało by na kliknięty btn
            //dzięki zastosowaniu funkcji strzałkowej, pod this
            //trafił zewnętrzny kontekst, czyli nasz obiekt ob

            console.log(this.name); //Batman
        });
    }
}

ob.btn();

Jak pamiętasz, problem zmiany kontekstu, rozwiązywaliśmy na dwa sposoby - albo poprzez użycie dodatkowej zmiennej that, albo poprzez zastosowanie bind():


const ob = {
    name : "Batman",

    printDelay() {
        const self = this;

        window.setTimetout(() => {
            console.log(self.name); //Batman
        }, 500);
    }
}

const ob = {
    name : "Batman",

    printDelay() {
        window.setTimetout(() => {
            console.log(this.name); //Batman
        }.bind(this), 500);
    }
}

To samo możemy uzyskać za pomocą funkcji strzałkowej:


const ob = {
    name : "Batman",

    printDelay() {
        window.setTimetout(() => {
            //normalnie this wskazywało by na window bo jest przed kropką
            //dzięki funkcji strzałkowej this wskazuje na zewnętrzny kontekst
            //czyli ob
            console.log(this.name); //Batman
        }, 500);
    }
}

ob.printDelay();

Kiedy stosować a kiedy nie

Kiedy więc stosować funkcję strzałkową a kiedy nie?

Jeżeli w danej funkcji nie używamy this, wtedy śmiało używajmy skróconego zapisu (jeżeli oczywiście nam pasuje, bo nic na siłę).

Na pewno też przy funkcjach wyższego rzędu (forEach, map, filter, sort itp) będzie to dobry wybór.

Nigdy natomiast nie stosujmy strzałki przy tworzeniu metod danego obiektu czy metod prototypu (chyba że nie odwołują się do this):


//w poniższych funkcjach nie używam this - śmiało strzałka
const myPrint = (text, nr) => console.log(text.repeat(nr));
const mixLetter = (text) => text.map((el,i) => (i%2 === 0) ? el.toUpperCase() : el.toLowerCase());   

//funkcja iterująca - śmiało strzałka
[1,2,3,4].mam(el => el * 2);

const elements = document.querySelectorAll('button');
elements.forEach(el => console.log(el.innerText));

const ob = {
    name : "Marcin",

    //metoda obiektu operująca na this - nie wolno strzałki, bo by zmieniło kontekst this
    print() {
        console.log(this.name);
    },

    //metoda nie odnosi się do this - może być strzałka, ale trzymajmy spójność
    sayHello() {
        console.log("Hello World!");
    }
}

const ob = {
    name : "Marcin",
    pet : "Szamson",

    //metoda w której odwołuję się do this
    buttons() {
        console.log(this);

        //nie chcę by this wskazywało na button tylko na ob
        //dlatego tutaj koniecznie strzałka
        document.querySelector('button').addEventListener('click', () => {
            console.log(this.name, this.pet);

            setTimeout(() => {
                //normalnie w setTimeout this wskazuje na window
                //czemu? Bo tak naprawdę powyżej napisaliśmy window.setTimeout
                //my tego nie chcemy więc strzałka
                console.log(this.name, this.pet);
            }, 2000);
        });
    }
}

Trening czyni mistrza

Poniżej zamieszczam kilka zadań, które w ramach ćwiczenia możesz wykonać:

  1. Ściągnij sobie plik zadanie-funkcja-strzalkowa.html (prawy klik na link, zapisz jako...).
    Otwórz plik w edytorze.
    W pliku tym przygotowałem prosty skrypt, który pobiera dane z serwera. Twoim zdaniem jest popracować na tych danych. Reszta instrukcji znajduje się w kodzie html.
  2. stwórz obiekt ob, który będzie miał:

    - właściwość favoriteColors - tablica z kolorami (mogą być pisane słownie, mogą hexdecymalnie)
    - metodę bind, w której pobierzesz ze strony jakiś element ( np. przycisk - jak go nie masz, dorób).

    Po kliknięciu na pobrany element, stwórz w pętli nowe elementy:

    - div o wymiarach 100x100
    - border: 1px solid #333
    - display:inline-block
    - tło pobrane z tablicy favouriteColors

    Stworzone elementy wstaw na stronę
    
                    const ob = {
                        favoriteColors : ["red", "blue", "green", "yellow"],
    
                        bind : function() {
                            document.querySelector("#button").addEventListener("click", () => {
                                //jeżeli nie użyjemy funkcji strzałkowej, wtedy
                                //this wskaże na #button a nie na ob
                                //inną metodą jest użycie .bind() lub dodatkowej zmiennej const that = this;
                                const body = document.querySelector("body");
    
                                this.favoriteColors.forEach(color => {
                                    const div = document.createElement("div");
                                    div.style.width = "100px";
                                    div.style.height = "100px";
                                    div.style.border = "1px solid #333";
                                    div.style.display = "inline-block";
                                    div.style.backgroundColor = color;
    
                                    body.appendChild(div);
                                });
                            });
                        }
                    }
    
                    ob.bind();