W niniejszym artykule przedstawię wszystkie aspekty używania funkcji w języku
JavaScript. Omówię czym się różni funkcja od procedury, co to są typy
pierwszo-klasowe, funkcje wyższego rzędu, domknięcia leksykalne, funkcje jako
metody oraz kontekst funkcji czyli zmienna specjalna this.
Funkcje a procedury
Funkcja czyli odpowiednik matematycznej funkcji jest to obiekt, który
dla tego samego argumentu zawsze zwraca tą samą wartość. Z punktu widzenia
programistycznego tego typu funkcje są nazywane czystymi (ang. pure),
nie wykonują one żadnych dodatkowych czynności, tzn. nie mają efektów ubocznych
(ang. side effects).
Procedura natomiast jest to pewien wykonywany proces, np. dwie funkcje,
które używają innego sposobu na obliczenie jakiejś wartości będą z matematycznego
punktu widzenia tą samą funkcją, natomiast będą to dwie różne procedury
na obliczenie danej wartości.
W programowaniu przyjęło się, że procedura to funkcja, która nie zwraca
wartości, głównie z powodu języka Pascal, którego uczą w szkołach.
Rekurencja
Funkcje tak jak w wielu innych językach mogą wywoływać same siebie, aby tworzyć
funkcje rekurencyjne. Np aby obliczyć silnie (częsty przykład stosowany do opisu
rekurencji), można użyć funkcji poniżej:
function factorial(n) {
if (n <= 0) {
return 1;
} else {
return n*factorial(n-1);
}
}
factorial(10);
// ==> 3628800
Funkcje są typem pierwszo-klasowym
W języku JavaScript funkcje są typem pierwszo-klasowym oznacza, funkcja jest
takim samym obiektem jak np. liczba, czy ciąg znaków. Można je umieszczać
w tablicach, przypisywać do zmiennych, przekazywać jako argumenty do funkcji.
Można także tworzyć funkcje, które zwracają funkcje. Są to tzw. funkcje wyższego
rzędu.
Funkcje wyższego rzędu
Najbardziej znanymi przykładami funkcji wyższego rzędu są funkcje takie jak: map, filter, reduceczy forEach, które zostały dodane do wersji
ES5 języka JavaScript. Wersja ES5 powinna być dostąpna w każdej nowej przeglądarce
internetowej. Funkcje te są dostępne jako metody tablic ponieważ operują one
właśnie na tablicach.
Dzięki funkcji map możemy zamienić tablicę na nową tablicę przetworzoną
w pewien sposób np:
var integers = [1,2,3,4,5,6,7,8];
var integers_plus_10 = integers.map(function(integer) {
return integer+10;
});
console.log(integers_plus_10);
// => [11, 12, 13, 14, 15, 16, 17, 18]
W powyższym kodzie nowa tablica będzie zawierała listę liczb, w której każda
została zwiększona o 10. Funkcja filter zwraca nowa tablicę, w której znajdą
się tylko takie elementy dla, których funkcja przekazana jako argument zwróci
wartość true. Funkcja reduce łączy wszystkie elementy ze sobą. W wyniku
jej działania otrzymujemy jedną wartość, dzięki niej możemy obliczyć np. sumę
liczb:
integers.reduce(function(sum, integer) {
return sum+integer;
});
// ==> 36
Funkcja forEach działa jak map, przetwarzając każdy element tablicy nie
zwraca ona jednak nowej tablicy.
Inną ciekawą funkcją, którą można użyć jako przykład funkcji wyższego rzędu jest
funkcja curry, często jako przykład kodu implementującego tą funkcję podaje
się błędnie partial application. Funkcja curry przyjmuje funkcje jako
argument a wynikiem jest seria funkcji, w której każda przyjmuje jeden argument
dopóki wszystkie argumenty nie zostają wyczerpane wtedy wywoływana jest nasza
oryginalna funkcja i zwracany jest wynik. Kod funkcji curry przedstawiono poniżej:
function curry(fn) {
var args = [];
return function curring() {
args = args.concat([].slice.call(arguments));
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return curring;
}
};
}
Można ją użyć np w ten sposób:
function add(a, b, c, d) {
return a+b+c+d;
}
curry(add)(1)(2)(3)(4);
// ==> 10
Ciekawszym rozwiązaniem jest funkcja, która działa podobnie ale na każdym etapie
można wywoływać funkcję z wieloma argumentami lub bez argumentów, tego typu
funkcja dostępna jest w bibliotece wu.js
i nazywa się autoCurry. Zachęcam do zajrzenia do kodu źródłowego biblioteki.
Funkcje bez nazwy
Jak widzieliście w poprzednich przykładach z funkcją map czy reduce, można
zadeklarować funkcję bez nazwy, jest to tzw. funkcja anonimowa. W języku JavaScript
w zależności od miejsca, w którym się znajdzie deklaracja, funkcja może być traktowana
jako wyrażenie lub instrukcja. Jeśli wstawimy ją samą mimo że nie będzie miała nazwy
będzie to instrukcja dlatego nie trzeba na końcu wstawiać średnika.
function() {
}
Jeśli natomiast przypiszemy ją do zmiennej będzie to już wyrażenie:
var foo = function() {
};
Domknięcia leksykalne
Domknięcia leksykalne (ang. closures) są to funkcje, wewnątrz których mamy dostęp
do zmiennych, które zostały zadeklarowane na zewnątrz funkcji mimo że zakres ich
istnienia się już zakończył. Istnieją one w środowisku, które jest “doczepione”
do funkcji. Często spotykanym przykładem jest licznik:
function counter(init) {
var start = init;
return function(inc) {
inc = inc || 1;
start+=inc;
return start;
};
}
var start10 = counter(10);
var start2 = counter(2);
console.log(start10());
// ==> 11
console.log(start10(5));
// ==> 16
console.log(start2(2));
// ==> 4
console.log(start2(2));
// ==> 6
W powyższej funkcji zmienna start zakończyła swój żywot, ale jej referencja
jest zamknięta wewnątrz środowiska, do którego ma dostęp funkcja anonimowa, która
została zwrócona przez counter. Powyższą funkcję można trochę uprościć:
function counter(init) {
return function(inc) {
return init+=inc||1;
};
}
IIFE – Immediately-Invoked Function Expression
Natychmiastowo-wywoływane wyrażenie funkcyjne, jest często stosowane w języku
JavaScript, ponieważ w języku tym bloki nie tworzą nowego zakresu zmiennych, tak
jak to ma miejsce w przypadku języka Java czy C. Stosuje się je także
aby odizolować część kodu od reszty programu. Często stosują ją twórcy bibliotek
np. cały kod biblioteki jQuery znajduje się wewnątrz takie funkcji, tworząc
pluginy jQuery często stosujemy poniższy kod:
(function($) {
$.fn.plugin_name = function() {
// kod pluginu
};
})(jQuery);
Dzięki temu możemy wewnątrz tworzyć prywatne zmienne dostępne tylko z wnętrza
naszego pluginu. Dodatkowo jeśli użytkownik korzystający z pluginu używa wywołaniajQuery.noConflict() w naszym kodzie nadal będziemy mieli dostęp do zmiennej
dolar ponieważ jest to zmienna lokalna (dostępna jako parametr IIFE).
W powyższym przykładzie funkcja jest wyrażeniem ponieważ jest objęta za pomocą
nawiasów, istnieje kilka sposobów wymuszenia, aby funkcja była wyrażeniem,
często spotyka się także użycie wykrzyknika na początku:
!function() {
}();
W języku JavaScript często stosuje się tego typu funkcje wewnątrz pętli, aby
utworzyć środowisko leksykalne. Jeśli mamy poniższy kod:
for (var i = 0; i <= 9; i++) {
setTimeout(function() {
console.log(i);
}, i*1000);
}
// ==> 10
// ==> 10
// ==> 10
// ==> 10
// ==> 10
// ==> 10
// ==> 10
// ==> 10
// ==> 10
// ==> 10
To na konsoli zobaczymy co jedną sekundę 10 wartości 10 a nie jak byśmy się
spodziewali wartości od 0 do 10. Jest to spowodowane tym że pętla for nie tworzy
nowego środowiska i dla każdego wywołania console.log mamy tą samą referencje.
Po jednej sekundzie nasza pętla się już zakończy i w zmiennej i będzie się
znajdowała wartość 10, którą następnie wyświetlą wszystkie opóźnione funkcje.
Aby temu zapobiec stosuje się anonimową funkcje, która jest natychmiast wywoływana
wewnątrz, której zostanie stworzone nowe środowisko z naszą zmienną. Dzięki czemu
każde wywołanie console.log będzie miało swoją własną zmienną.
for (var i = 0; i <= 9; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
}, i*1000);
})(i);
}
// ==> 0
// ==> 1
// ==> 2
// ==> 3
// ==> 4
// ==> 5
// ==> 6
// ==> 7
// ==> 8
// ==> 9
Wewnątrz samowywołującego się wyrażenia funkcyjnego zmienna nie musi nosić nazwy i może to być np. x ale dzięki temu pokazujemy, że zmienna w pętli i zmienna
w console.log ma tą samą wartość.
Można także stosować wyrażenia funkcyjne, które mają także nazwę aby móc wewnątrz
funkcji odwołać się do samej siebie.
(function animation() {
render();
setTimeout(animation, 60);
})();
Istnieje także możliwość odwołania się do samej siebie wewnątrz funkcji za pomocą
zmiennej arguments.callee ale jest ona niedozwolona w trybie strict mode.
Funkcja jest obiektem, który może mieć właściwości
Funkcja jest takim samym obiektem jak np. ciąg znaków. Posiada wbudowane metody
i właściwości. Możemy także dodawać nowe funkcje i właściwości do pojedynczej
funkcji jak i do każdej funkcji dzięki dziedziczeniu prototypowemu, które
zostanie omówione w innym artykule.
Każda funkcja posiada między innymi wbudowaną właściwość length, która
określa liczbę parametrów, z którą została zadeklarowana czy name, która
określa nazwę funkcji. Funkcje posiadają takie metody jak bind, apply,
czy call, które zostaną omówione w kolejnej sekcji.
Wewnątrz funkcji mamy także dostęp do specjalnej zmiennej arguments, która
jest podobna do tablicy ale tablicą nie jest. Dzięki niej mamy dostęp do wszystkich
argumentów wywołanej funkcji, dzięki czemu możemy tworzyć funkcje ze zmienną liczbą
argumentów.
Kontekst funkcji oraz funkcja jako konstruktor
Funkcja może być także klasą znaną z innych obiektowych języków programowania
a dokładnie może być konstruktorem klasy. Same klasy nie istnieją w języku
JavaScript, będą natomiast wprowadzone w wersji ECMAScript 6, dostępne są także
w jezyku CoffeeScript, który kompiluje się do kodu JavaScript.
Aby utworzyć konstruktor piszemy zwykłą funkcje, mamy wewnątrz niej jednak
dostęp do naszego obiektu pod zmienną specjalną this.
function Person(name) {
this.name = name;
}
var jan = new Person('Jan Kowalski');
Tak jak w innych językach do utworzenia nowego obiektu stosuje się słowo kluczowe new. Jeśli go nie zastosujemy, zmienną this, czyli tzw. kontekstem
będzie obiekt window, czyli obiekt globalny.
Tak jak w innych językach obiektowych możemy tworzyć metody, czyli funkcje, które
wywołujemy w kontekscie jakiegoś obiektu. Możemy bezpośrednio dodać funkcje do
zmiennej this lub dodać do tzw. prototypu funkcji. Prototypy wykraczają poza
zakres niniejszego artykułu, omówimy je przy innej okazji. W poniższym kodzie
zdefiniowano metodę getName, która odwołuje się do pola name za pomocą
zmiennej this:
function Person(name) {
this.name = name;
this.getName = function() {
return this.name;
};
}
var jan = new Person('Jan Kowalski');
console.log(jan.getName());
// => Jan Kowalski
Możemy wywołać naszą metodę korzystając z notacji kropki. Jak jednak
dowiedzieliście się wcześniej funkcje są typem pierwszo-klasowym. Co stanie więc
gdy przypiszemy metodę do innej zmiennej i spróbujemy ją wywołać. Moglibyśmy czegoś
takiego potrzebować np. gdybyśmy chcieli przekazać metodę do funkcji wyższego rzędu.
function Person(name) {
this.person_name = name;
this.getName = function() {
return this.person_name;
};
}
var jan = new Person('Jan Kowalski');
var jan_name = jan.getName;
console.log(jan_name());
// ==> undefined
Wyświetli się wartość undefined, ponieważ zmienną this będzie znowu
obiekt globalny window, który nie ma zdefiniowanej zmiennej person_name.
Jeśli użyjemy trybu strict, gdy wywołamy funkcję jan_name zwrócony zostanie wyjątek: “TypeError: Cannot read property ‘person_name’ of undefined” ponieważ w trybie strict mode zmienna this gdy wywołana bez kontekstu jest zawsze niezdefiniowana.
function Person(name) {
"use strict";
this.person_name = name;
this.getName = function() {
return this.person_name;
};
}
var jan = new Person('Jan Kowalski');
var jan_name = jan.getName;
console.log(jan_name());
// ==> TypeError: Cannot read property 'person_name' of undefined
Tryb strict jest nową funkcją standardu ECMAScript 5, który nie pozwala na
pewne konstrukcje, wyrzucając więcej wyjątków. Zawsze dobrze jest mieć włączony
strict mode, na początku naszego programu, aby wyłapać kod, którego nie powinno
się stosować.
W jezyku JavaScript możemy zmieniać kontekst, czyli zmienną this wewnątrz
funkcji. Służą do tego trzy funkcje call, apply oraz bind. Funkcje call oraz apply są bardzo podobne wywołują one daną funkcje, zmieniając
kontekst. Do funkcji call przekazujemy listę argumentów po przecinku natomiast
do funkcji apply tablicę argumentów. Natomiast funkcja bind zwraca nową
funkcję, w której kontekst jest zmieniony.
W naszym poprzednim przykładzie aby wywołać naszą funkcje jan_name w kontekście
obiektu jan można skorzystać z jednego z poniższych wywołań:
console.log(jan_name.apply(jan));
console.log(jan_name.call(jan));
W obu przypadkach nie przekazano argumentów. Można także skorzystać z funkcji bind aby od razu utworzyć funkcję z właściwym kontekstem:
var jan_name = jan.getName.bind(jan);
Dzięki funkcji bind możemy przekazywać metody jako funkcje do innych funkcji.