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 chcemy 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 = function(a, b) { return a + b }

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

Aż tak wiele nam nie uprościło. Zamiast słowa function pojawiła się charakterystyczna strzałka.
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 i instrukcję 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;
    return ret;
}

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

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

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

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

Do czego to się w zasadzie przydaje?

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);

//spradzamy 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 damkie 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"

Zmiana kontekstu dla this

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

Najważniejszą sprawą w funkcji strzałkowej jest to, że zmienia ona kontekst this na zewnętrzny.
Spójrz na przykład poniżej:


const ob = {
    name : "Marcin",
    printName : function() {
        console.log(this.name);
    }
}

//kontra

const ob = {
    name : "Marcin",
    printName : () => {
        console.log(this.name);
    }
}

W pierwszym przypadku metoda printName działa klasycznie, więc this wskazuje na obiekt, do którego dana metoda należy.

W drugim przypadku this wskazuje na zewnętrzny kontekst, czyli w naszym przypadku na obiekt window!

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


const ob = {
    name : "Marcin",
    pets : ['dog', 'cat', 'pig'],

    bindBtn : function() {
        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); //ob
        });
    },

    printElements: function() {

        this.pets.forEach(function(el) {
            //this w tym konkretnym przypadku wskaże na window (bo tak działa forEach)
            //zeby zadziałało jak należy musielibyśmy użyć dodatkowego parametru (ostatni parametr metody forEach)
            //albo funkcji strzałkowej

            console.log(this); //window
        })

    }
}

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


//użycie dodatkowej zmiennej o nazwie that
bindBtn : function() {
    const that = this;

    const btn = document.querySelector('.btn');
    btn.addEventListener('click', function() {
        console.log(that);
    });
},

//użycie bind
bindBtn : function() {
    const btn = document.querySelector('.btn');
    btn.addEventListener('click', function() {
        console.log(this);
    }.bind(this));
},

//użycie funkcji strzałkowej
bindBtn : function() {
    const btn = document.querySelector('.btn');
    btn.addEventListener('click', () => {
        console.log(this);
    });
}

Pierwszy sposób nie jest równoznaczny metodzie bind() i użyciu funkcji strzałkowej. Czemu? Pamiętaj, że możemy w każdej chwili zawołać po taką metodę:


const ob2 = {};

ob.bintBtn.call(ob2);

Spowoduje to zmianę this w tej metodzie, więc pod zmienną that zostanie podstawione zupełnie coś innego.

Praktyczne przykłady

Nie wiem czy powyższe wywody są dla ciebie dostatecznie czytelne, dlatego poniżej przyjrzyjmy się jeszcze kilku przykładom:


const MyObj = function() {
    this.names : [],
}

MyObj.prototype.readDataFromServer : function() {
    //załóżmy, że poniższe dane przyszły z serwera
    const data = [
        {name : "marcin", age : 10},
        {name : "ania", age : 12},
        {name : "monika", age : 15}
    ]

    ...
}

W powyższym przykładzie mamy prosty obiekt, który ma metodę readDataFromServer. Zakładamy, że wczytuje ona dane z serwera. Naszym zadaniem jest zrobić po tych danych pętlę, a następnie do zmiennej names naszego obiektu wstawić imiona pisane dużymi literami


MyObj.prototype.readDataFromServer : function() {
    const data = [
        {name : "marcin", age : 10},
        {name : "ania", age : 12},
        {name : "monika", age : 15}
    ];

    data.forEach(function(el) {
        this.names...
    })

}

Ok przerwałem pisanie tego skryptu, bo i tak by nie zadziałał. Domyślnie we wnętrzu forEach this wskazuje na obiekt window. Żeby to zmienić powinniśmy użyć albo dodatkowego atrybutu:


MyObj.prototype.readDataFromServer : function() {
    ...

    data.forEach(function(el) {
        ...
    }, this);
}

Albo zastosować funkcję strzałkową:


MyObj.prototype.readDataFromServer : function() {
    ...

    data.forEach(el => {
        return this.names.push(el.toUpperCase());
    });
}

//lub w skróconej wersji

MyObj.prototype.readDataFromServer : function() {
    ...
    data.forEach(el => this.names.push(el.toUpperCase()) );
}

Czy znasz inny sposób rozwiązania powyższego zadania? Skoro mamy utworzyć nową tablicę z imionami pisanymi dużymi literami, o wiele bardziej praktycznie będzie po prostu użycie metody map():


MyObj.prototype.readDataFromServer : function() {
    ...

    this.names = data.map(el => el.toUpperCase());
}

Kolejny przykład pokazuje obsługę eventu w obiekcie:


const ob = {
    pets : ["kot", "pies", "borsuk", "chomik-ninja"],
    bindButton : function() {
        const btn = document.querySelector('.btn');
        btn.addEventListener('click', () => {
            e.target.innerText = "Kliknięto!"; //e.target = kliknięty guzik
            pets.forEach(el => console.log(el));
        })
    }
}

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();