JavaScript – mnohými milovaný, mnohými nenáviděný, mnohými nepochopený jazyk, mnohými všechno dohromady. Mnoho nejasností pramení z jeho práce s funkcemi a jejich volání. A proto si v dnešním článku ukážeme, jak to vlastně je.
Tento článek se nebude zabývat naprostými základy, jakými jsou např. definování proměnných, výrazy, operátory, řídící struktury apod. K těmto účelům jsou zde jiné zdroje [Kurz JavaScriptu, Seriál JavaScript].
Ač to možná někteří neví anebo to opomíjejí, funkce jsou v JavaScriptu tzv. „first-class“ hodnoty. Neboli se s nimi může nakládat jako s jakýmikoli jinými hodnotami – dají se ukládat do proměnných, předávat jako parametry funkcí a vracet z funkcí. Je to díky tomu, že funkce jsou vlastně objekty.
Javascriptové objekty jsou kapitola sama pro sebe, takže jenom stručně. JavaScript nepodporuje „klasickou“ dědičnost známou z dnes nejrozšířenějších jazyků podporujících objektově orientované programování – třídní dědičnost. JavaScript je založen tzv. dědičnosti prototypové, neboli beztřídní. To znamená, že se v něm objekty neřadí k žádným třídám, které by určovaly jejich chování, ale objekty jsou samostatné entity, kdy dva mohou třeba vykazovat naprosto stejné chování, ale není nijak určeno, že by měly.
Klíčem ke sdílení vlastností mezi objekty jsou tzv. prototypy. Může se k nim přistupovat pomocí vlastnosti prototype
každého objektu (tedy i funkcí).
Nyní se již vrhněme k tomu, jak definovat funkci.
// 1. vytvořením funkčního objektu var add = new Function("a", "b", "return a + b;"); // 2. funkčním literálem var add = function (a, b) { return a + b; }; // 3. funkčním literálem se syntaktickým cukrem function add(a, b) { return a + b; }
Všechny tři zápisy jsou ekvivalentní. První způsob (pomocí vytvoření nového funkčního objektu) není moc obvyklý, proto ho berme jen jako zajímavost a dál se jím již nezabývejme. Druhý mezi některými také nemusí být příliš obvyklý, ale je velice názorný v tom, jak zvýrazňuje hlavní vlastnost javascriptových funkcí – jejich hodnost jako „first-class“ objektů. Třetí je nejspíš nejobvyklejší, protože syntaktický cukr je přeci jen hezká věc, a navíc jelikož pro mnoho lidí není JavaScript jejich „hlavní“ jazyk, ale většinou jen spíše pomůcka, jak zinteraktivnit jejich aplikace, tento zápis jim připomíná syntaxi jejich jazyka. Srovnejme třetí příklad definice kupř. s PHP:
function add($a, $b) { return $a + $b; }
Po zbytek článku bude používána druhá metoda definice funkcí, aby byla zvýrazněna jejich „objektovost“ :o). Hlavní využití třetího způsobu bych ponechal pro rekurzivní funkce. Jelikož pak můžeme například dělat takovéhle kusy:
var return_factorial = function () { return function fac(n) { if (n < 2) { return 1; } else { return n * fac(n - 1); } }; }; var my_factorial = return_factorial(); alert("5! = " + my_factorial(5));
Praktické využití tohoto příkladu je nulové. Takže pokud vás napadne nějaké lepší, budu rád, pokud se podělíte v diskusi pod článkem s nějakým lepším.
Volání funkcí
Právě u volání funkcí někdy bývají nejasnosti, hlavně co se týče toho, k jaké hodnotě se bude vázat funkční proměnná this
. Dokonce existují čtyři způsoby volání funkcí. Pojďme se tedy na ně podívat.
Funkční volání
Prvním a asi nejjednodušším voláním je „funkční volání“ (funcion invocation pattern). Je to totéž, co jsme doposud ve článku mohli vidět. Při funkčním volání je v this
uložen tzv. „globální objekt“. Snad vždy se jedná o objekt window
. (Pokud byste věděli více, určitě se vyjádřete pod článkem.) Pro příklady funkčního volání se podívejte na ukázky kódu výše. Dá se říci, že při funkčním volání je nám this
všeho všudy k ničemu.
Volání s operátorem new
Operátor new
slouží, podobně jako v jiných jazycích, k vytváření objektů. Při přidání new
před volání funkce se tak funkce stává tzv. „konstruktorem“, tedy metodou objektu, která inicializuje jeho stav. Jelikož v JavaScriptu neexistují třídy, všechno je řešeno pomocí prototypů. Takže při volání konstruktoru se vytvoří nový prázdný objekt, kterému se do prototypu zkopíruje prototyp volané funkce, a pak
se již vykonává samotný konstruktor, kdy hodnotou proměnné this
je ten nově vytvořený objekt.
var Cat = function (name) { this.name = name; }; var my_cat = new Cat("lucy"); alert(my_cat); alert(my_cat.name); // lucy
Velký problém nastává tehdy, když je volána „konstruktorová“ metoda ne jako konstruktor – metoda stavěná na to, aby byla volána s operátorem new
je volána bez tohoto operátoru. To se pak veškerá inicializace provede do globálního objektu a jelikož konstruktory jen málokdy vrací nějaké hodnoty (při volání s operátorem new
je taková hodnota, pokud se nejedná o objekt, stejně zahozena a vrácen je nový objekt), dostane se nám do proměnné, která měla obsahovat inicializovaný objekt, hodnota undefined
.
Volání jako metody objektu
Třetím případem volání může být takové, pokud máme nějaký objekt a pomocí tečkové notace (.
) přistupujeme k jeho vlastnosti (objekt.vlastnost
) a voláme ji jako metodu (objekt.vlastnost()
). Je nejspíš logické, co se pak bude skrývat pod this
– ano, objekt, nad kterým je metoda volána (this === objekt
). Pro pochopení bude asi nejlepší rozšířit příklad s naší kočkou.
Cat.prototype.sayMeow = function () { alert(this.name + " says meow!"); }; var my_cat = new Cat("lucy"); my_cat.sayMeow(); // lucy says meow!
Volání pomocí call()
a apply()
Jak již bylo řečeno, funkce jsou vlastně objekty, takže na nich jdou také volat metody. Jedněmi z nich, zděděné
z funkčního prototypu, jsou metody call()
a apply()
. Obě jsou si prakticky stejné – slouží
k vyvolání funkce. První parametr obou funkcí je objekt, který bude přiřazen do this
. Jediný rozdíl
mezi oběma funkcemi je v tom, že při volání call()
jsou parametry pro volanou funkci všechny parametry
call()
krom prvního (první parametr je this
), zatímco apply()
přijímá parametry
v poli jako druhý argument.
add.call(null, 2, 3); // 5 add.apply(null, [2, 3]); // 5
Argumenty
Všechny argumenty funkcí jsou v JavaScriptu nepovinné a nelze nijak zajistit, aby povinné byly. Na jednu stranu to může být dobré, na druhou zase moc ne. Parametry deklarované při definici funkce jsou jen formální a slouží hlavně k tomu, abychom se na ně ve funkci mohli odkazovat jménem.
Jinak je v každé funkci totiž přítomen objekt arguments
, který obsahuje pod číselnými indexy všechny argumenty funkci předané (první argument je na indexu 0
). Můžeme říct, že je to něco jako pole, ale pole samotné (objekt Array
) to není (což je mimochodem veliká designová chyba jazyka, ale nadělat s tím moc nemůžeme). Nad arguments
i přesto, že obsahuje vlastnost length
jako pole, nemůžeme volat metody pole.
var sum = function () { var i, sum = 0; for (i = 0; i < arguments.length; ++i) { sum += arguments[i]; } return sum; }; sum(1, 2, 3, 4, 5); // 15
Platnost proměnných a closures (uzávěry)
JavaScript na rozdíl od jiných jazyků nemá příliš propracovaný systém bloků, ale jak se říká, v jednoduchosti je síla. Jelikož zde nejsou žádné jmenné prostory, žádné třídy ani nic podobného, základní jednotkou oboru platnosti proměnných je funkce. (Kromě funkce je zde samozřejmě také, jak bývá zvykem, globální prostor jmen.)
Jelikož se mohou funkce zanořovat – lze definovat funkci uvnitř těla jiné funkce –, každá taková zanořená funkce má dostupné všechny proměnné, které jsou dostupné z funkce nadřazené.
var d = 8; var foo = function () { var a = 1, b = 2; // a = 1, b = 2, d = 8 d = 10; // a = 1, b = 2, d = 10 var bar = function () { var b = 1, c = 8 // a = 1, b = 1, c = 8, d = 10 a = 5; b = 3; d = 20; // a = 5, b = 3, c = 8, d = 20 }; bar(); alert("a = " + a); // a = 5 alert("b = " + b); // b = 2 // c neexistuje }; foo(); alert("d = " + d); // d = 20
S vědomím tohoto se dají vyrábět tzv. closures, což jsou funkce, které v sobě obsahují kontext z jiné funkce, ve které byly definované, i když vykonávání jejich „mateřské“ funkce už skončilo. Zní to složitě, takže lepší bude nějaký příklad. Řekněme, že potřebujeme objekt reprezentující osobu. Jelikož lidé mohou růst, bude zde samozřejmě možnost postupně časem měnit výšku takové osoby. První řešení, jaké by nás mohlo napadnout, by mohlo vypadat takto:
var person = function (meters) { this.height = meters; };
Osobu vytvoříme voláním funkce person()
s operátorem new
(aby se vytvořil nový objekt,
který bude přiřazen do this
). K výšce můžeme po vytvoření přistupovat pomocí vlastnosti height
. Je to velice jednoduché, ale je zde problém, že výšku nemůžeme nijak regulovat. Takže se nám zde pak klidně může vyskytnout osoba měřící místo pár metrů i pár stovek (kilo)metrů, ba dokonce i nějaká, která by „rostla do země“ (výška by byla záporná), či persona nulového vzrůstu (i takový Kulihrášek musel mít přinejmenším pár milimetrů). Dalším řešením by mohlo být zavedení tzv. „getterů“ a „setterů“, takže by stačilo přidat:
person.prototype.getHeight = function () { return this.height; }; person.prototype.setHeight = function (meters) { if (meters > 0 && meters < 5) { this.height = meters; return true; } return false; };
Mohlo by se zdát, že problém je vyřešen, ale jelikož JavaScript nezná žádnou viditelnost vlastností objektů, pořád můžeme měnit výšku i jinak než přes její setter – stačí normálně změnit vlastnost height
. Řešením jsou closures.
var person = function (meters) { this.getHeight = function () { return meters; }; this.setHeight = function (new_meters) { if (new_meters > 0 && new_meters < 5) { meters = new_meters; return true; } return false; }; };
Nyní se již nemůžeme k výšce dostat jinak než pomocí getteru a setteru.
Na závěr
Funkce jsou až na pár malých nedostatků nejsilnější zbraní JavaScriptu, který je hlavně díky nim nazýván „Lispem v kabátě Céčka“. Samozřejmě jako se všemi mocnými zbraněmi je potřeba umět s nimi zacházet správně. Doufám, že vám tento článek poskytl lepší představu, jak tak činit.