A függvények a JavaScript nyelv talán legfontosabb elemei. Ugyanakkor a függvények azok, amik a legtöbb félreértés áldozatai. Ennek okán úgy gondolom érdemes tisztázni a függvények működését, és használatát. A függvények a JavaScriptben elsődleges (first-class) típusok, azaz hozzárendelhetjük őket változókhoz és objektum tulajdonságokhoz, átadhatjuk őket paraméterként más függvényeknek, valamint egy függvény illetve metódus is térhet vissza függvénnyel (egy függvényt metódusnak nevezünk, ha az egy objektum tulajdonsága). A klasszikus programozási nyelvekhez képest (C/C++, Java, PHP) ez a megközelítés idegen, ez is okozhatja a sok problémát az értelmezéssel kapcsolatban.
Alapvetően háromféleképpen hozhatunk létre függvényeket:
- függvény deklarációval (function declaration)
- függvénykifejezéssel (function expression)
Function
konstruktor használatával.
A függvények létrehozásának ez a módja talán a legelterjedtebb.
function peldaFuggveny(parameter) {
// függvény törzs
}
Ez a formája a függvényeknek szinte teljesen megegyezik a klasszikus nyelvekben való felírással, azzal a különbséggel, hogy mivel a JavaScript egy szkript nyelv, ezért nincs megadva a változók típusa, valamint hogy a függvény milyen típusú változót ad vissza, sőt az sem, hogy egyáltalán ad-e vissza valamit.
A függvény deklaráció szabályai a következők:
- A függvény deklarációnak kötelezően van neve,
- vagy Program szinten, vagy egy másik függvény törzsében szerepel,
- akkor jön létre, amikor a program futása abba a környezetbe jut,
- módosítja a környezeti változók listáját (variables object).
Ebből a listából szerintem az első pont nem szorul magyarázatra. A 2. pont azt jelenti, hogy vagy a fő programkódban (böngésző esetén közvetlenül <script>
elemben), illetve egy abban levő függvény törzsében szerepelhet. Például:
<script>
function peldaFuggveny1(parameter) {
// függvény törzs
}
</script>
illetve
function foo() {
function peldaFuggveny2() {
// függvény törzs
}
}
A 3. pont pedig azt jelenti, hogy a peldaFuggveny2
nevű függvény akkor jön létre, amikor a foo
függvény futása elkezdődik. Ezt a viselkedést – azaz, hogy a függvény felliftezik a környezet tetejére – nevezzük hoisting-nak. Azaz:
function foo() {
var bar = peldaFuggveny2(); // a függvény már létezik
function peldaFuggveny2() {
// függvény törzs
}
}
A 4. pont szerint pedig az elérhető változók listája kibővül a függvény nevével. Azaz egy változóhoz hozzárendelhetjük a függvényt, valamint bármilyen más helyen, ahol kifejezést használhatunk, hivatkozhatunk a függvényre a nevével.
A függvénykifejezés (function expression) létrehozásának szabályai a következők:
- a kódban ott szerepelhet, ahol kifejezés szerepelhet,
- neve elhagyható,
- nem módosítja a környezeti változók listáját,
- akkor jön létre, amikor a program futása rákerül.
Úgy gondolom, hogy az 1. pont szorul a legnagyobb magyarázatra. Kifejezés szerepelhet függvények, illetve konstruktorok paramétereként, és mindenhol máshol, ahol változó szerepelhet. Mit jelent az, hogy a neve elhagyható? Hogyan hivatkozunk akkor majd a függvényünkre? Hát például úgy, hogy hozzárendeljük egy változóhoz, elvégre a függvények elsődleges típusok.
// Hozzárendelhetjük változókhoz.
var foo = function () {
// függvény törzs
};
// Átadhatjuk paraméterként.
foo(function () {
// függvény törzs
});
Ha a függvénykifejezésnek adhatunk nevet, de nem módosítja a környezeti változókat, akkor mire jó? Arra, hogy hivatkozni tudjunk rá a függvényünkön belül.
var foo = function bar(foobar) {
if (foobar) {
bar(!foobar);
}
};
A függvénykifejezés egyik speciális fajtája az azonnal meghívott függvénykifejezés (Immediately-Invoked Function Expression [IIFE]), vagy ahogy tévesen régebben nevezték self-executing anonymous function illetve self-invoked anonymous function. Ennek lényege, hogy a függvénykifejezést azonnal meghívjuk.
(function () {
// függvény törzs
})();
Hogyan is működik ez, és miért? A zárójelek hatására a függvényünk függvénykifejezés lesz, mivel a zárójeleken belül csak kifejezés szerepelhet (amennyiben a függvény már eleve olyan helyen szerepel, ahol csak kifejezés szerepelhet, a zárójelek elhagyhatók). Ezt a létrejött függvénykifejezést pedig azonnal meg is hívjuk.
(function foo(bar) {
if (bar) {
foo(false);
}
})('foobar');
Mint látható az azonnal meghívott függvénykifejezésnek nem kell anonimnak lennie, ha hivatkozni szeretnénk rá belülről, akkor adhatunk neki nevet is. Még egyszer tekintsük át, mi is történik itt valójában:
- Létrehozunk egy függvénykifejezést, aminek
foo
a neve, de kívülről nem elérhető. - A függvénykifejezést meghívjuk
'foobar'
paraméterrel. - A függvénykifejezés meghívja magát még egyszer (most már
false
paraméterrel), mivel tud saját magára hivatkozni a nevével.
Használatukkal elkerülhetjük a globális névtér változókkal való telepiszkítását. Ugyanis a függvényen belül var
kulcsszóval létrehozott változók a függvényen kívül nem léteznek. Ugyanakkor a függvényünk azonnal lefut, és még a függvény a saját nevével se szennyezi a globális névteret, mivel a függvénykifejezés neve nem kerül a környezeti változók közé.
Az Microsoft JavaScript implementációja (JScript) nem specifikáció szerint működik, amikor függvénykifejezések nevéről van szó. Ugyanis ha egy függvénykifejezésnek nevet adunk, akkor azt a JScript függvény deklarációnak gondolja teljesen hibásan, azaz a környezeti változók közé is bekerül a függvény. Ezért amennyiben Internet Explorer, vagy más JScript felhasználás is a megcélzott platformok között van, akkor bánjunk óvatosan a függvénykifejezések elnevezésével (például ne ütközzön változó, vagy függvénynevekkel).
Mivel a Function
konstruktor használata kerülendő, nem is foglalkozunk vele túl sokat, igazából csak az alapokat szeretném bemutatni.
var foo = new Function('bar', 'foobar', 'return bar + foobar;');
A Function
konstruktor meghívásakor az átadott paraméterek közül az utolsó lesz a függvény törzse, az előtt opcionálisan megadott paraméterek pedig a függvény paraméterei. Fontos megjegyezni, hogy a new
kulcsszó megadása nem kötelező, nem befolyásolja a működést.
Mert a megadott függvény nem abban a környezetben fut le, ahol futtatjuk, hanem a globális névtérben, ezért hozzáférhet a globális névtérben megadott változókhoz, de azokhoz nem, ahol fut.
var foo = 10;
function bar() {
var foo = 20,
foobar = new Function('alert(foo);');
foobar(); // 10;
}
bar();
Egy függvény visszatérhet futásának bármelyik pontján a return
utasítás segítségével. Amennyiben nem adtunk meg visszatérési értéket (üres return
utasítás), illetve a függvény legvégén nem adtunk ki return
utasítást, akkor a függvény undefined
értékkel tér vissza. A függvény visszatérhet akármilyen értékkel, legyen az valamilyen primitív típus, objektum, vagy akár egy függvény is.
function foo(bar) {
if (bar === true)
return 10;
else if (bar === false) {
return function () {
return false;
};
}
// implicit return undefined
}
Ha egy függvényt a new
kulcsszóval hívunk meg (azaz konstruktorként), akkor a függvény – amennyiben nem adunk ki return
utasítást –, akkor az aktuális this
értékkel tér vissza; és amennyiben nem adunk át paramétereket, akkor a meghívásnál a zárójelek elhagyhatók.
function Foo() {
// implicit return this
}
new Foo;
A függvények lehetővé teszik az úgynevezett closure (lezárt) használatát. A closure lényege, hogy hozzáférünk a függvényen kívüli változókhoz azután is, hogy a függvényen kívüli környezet már lefutott. Legegyszerűbben egy példával lehet bemutatni a működést.
function foo() {
var bar = 'foobar';
return function barfoo() {
alert(bar);
}
}
var foobar = foo();
foobar(); // 'foobar'
A fenti példában létrehoztunk egy foo
nevű függvényt, ami visszaad egy függvényt. A visszaadott barfoo
függvény – amit a foobar
nevű változóban tároltunk el –, továbbra is hozzáfér a bar
nevű változóhoz, holott a foo
függvény már régen lefutott. Lássuk a következő, igencsak klasszikus példát:
var foo = [], i, j;
for (i = 0; i < 5; i += 1) {
// Függvényeket pakolunk bele a foo tömbbe.
foo.push(function () { return i; });
}
for (j = foo.length - 1; j >= 0; j -= 1) {
// Kiírjuk a konzolra a függvény futásának eredményét.
console.log(foo[j]());
}
A fenti kis program 5 függvényt pakol bele a foo
nevű tömbbe. A függvény visszaadja az i
értékét. Ezután végigmegyünk fordított sorrendben a tömb elemein, és kiíratjuk a függvény futásának eredményét a konzolra. És egyesek meglepetésére minden esetben a konzolra az 5
íródik ki. Miért? Azért mert a függvényünk a closure-t használva hozzáfér az i
értékéhez, és az első for
ciklus futása után i
értéke 5
. Hogyan „javíthatjuk” meg a működést, azaz hogy a függvényünk azt az értéket adja vissza, mint ami i
volt a létrejöttekor?
var foo = [], i, j;
for (i = 0; i < 5; i += 1) {
// Függvényeket pakolunk bele a foo tömbbe.
foo.push(
// IIFE-et hozunk létre, ami bezárja egy closure-be i értékét
function (bar) {
// Ezt a függvényt fogjuk ténylegesen bepakolni a tömbbe
return function () {
// a függvény a bar változót adja vissza, ami i akkori értéke
return bar;
}
}(i)
);
}
for (j = foo.length - 1; j >= 0; j -= 1) {
// Kiírjuk a konzolra a függvény futásának eredményét.
console.log(foo[j]());
}
Most már a foo
tömbbe pakolt függvények hozzáférnek i
eredeti értékéhez (bar
-hoz), mivel azt egy closure-be zártuk. További információt az MDC Closures illetve Dmitry A. Soshnikov Closures oldalain találunk.
A függvényeken belül a this
értéke más nyelvekhez képest több dolgot jelenthet. Lássunk először is egy példát:
function foo() {
console.log(String(this));
}
foo();
var bar = {
foobar: foo
};
bar.foobar();
A fenti kódot böngészőben futtatva a következő kimenetet kapjuk:
[object Window]
[object Object]
Hogy is van ez? Alapvetően azok a függvények, amik nem egy objektum metódusai a window
, vagy más környezetben, más globális névtér objektumot adnak vissza, this
néven. Ilyen a foo
függvény is. Ugyanakkor, ha mint a bar
objektum metódusaként hívjuk meg, akkor azt az objektumot veszik this
-nek, aminek a metódusaként hívtuk meg (példánkban ez a bar
). Most fordítsuk meg a fenti példát:
var bar = {
foobar: function () {
console.log(String(this));
}
},
foo = bar.foobar;
foo();
bar.foobar();
A fenti leírás után remélhetőleg nem lepődik meg senki, hogy ugyanazt a kimenetet kapjuk, mint először. Az ok igencsak egyszerű. A foo
függvény ugyan a bar
objektum egy metódusa, de nem, mint a bar
objektum metódusa hívtuk meg, ezért a globális névtér objektum (a böngészőkben ez a window
) lett a this
.
Minden függvény a JavaScriptben egyben objektum is, és van pár metódusa is. Ilyen metódus a call
és az apply
. Hogy ne legyen egyszerű az élet, a this
értékét módosítani is tudjuk ezen metódusok használatával.
function foo(arg1, arg2) {
console.log(String(this), arg1, arg2);
}
var bar = {};
foo.call(bar, "foobar", "barfoo"); // [object Object] foobar barfoo
foo.apply(bar, ["foobar", "barfoo"]); // [object Object] foobar barfoo
A függvények call
metódusa első paraméterként a this
-t várja, a többi paramétert pedig a függvény hagyományos paraméterként kapja meg. Az apply
metódus annyiban különbözik, hogy egy tömbszerű objektumot vár, mint második paraméter, és annak elemeit adja át a függvénynek paraméterként.
Amennyiben a call
, illetve apply
első paramétere null
, illetve undefined
, akkor a this
a globális névtér objektumra fog mutatni. Amennyiben valamilyen primitív érték (String, Number, Boolean) akkor az objektumként adódik át, mint this
azaz lefut rajta a megfelelő típus konstruktora.
var foo = [null, undefined, NaN, true, 0, 'string', [], {}],
i, l;
function bar(arg) {
console.log('this: ', String(this), typeof this, ' | arg: ', arg, typeof arg);
}
for (i = 0, l = foo.length; i < l; i += 1) {
bar.call(foo[i], foo[i]);
}
Kimenetünk:
this: [object Window] object | arg: null object
this: [object Window] object | arg: undefined undefined
this: NaN object | arg: NaN number
this: true object | arg: true boolean
this: 0 object | arg: 0 number
this: string object | arg: string string
this: object | arg: [] object
this: [object Object] object | arg: Object {} object
A strict mode használata jelentősen módosít ezen a működésen. Jelenleg strict mode használatára nem sok lehetőség van, de azért megemlítem, miben változnak a dolgok. A call
, illetve apply
nem módosítja az átadott paraméter típusát, azaz a null
vagy undefined
értéket első paraméterként átadva null
, illetve undefined
lesz a this
értéke. Ehhez hasonlóan a primitív típusok se alakulnak át objektummá. Ennek megfelelően, amennyiben nem adtunk meg explicit this
értéket a függvény meghívásakor, illetve nem egy objektum metódusaként hívjuk meg, akkor a this
nem a globális objektum lesz, hanem undefined
. További információt a strict mode működéséről az MDC illetve Dmitry A. Soshnikov oldalain találunk.
Egy függvény argumentumaihoz a függvényen belül az arguments
objektumon keresztül is hozzáférhetünk. Ez akkor hasznos, amikor változó számú paramétert vár a függvény, illetve ki akarjuk deríteni, hogy ténylegesen mennyi paramétert is adtak át neki a meghíváskor.
function foo() {
console.log(arguments);
}
foo('bar', 'foobar'); // ["bar", "foobar"]
Ugyan az arguments
objektum hasonlít egy tömbre, de sajnos(?) nem az. Rendelekezik egy length
tulajdonsággal, valamint az egyes argumentumokhoz hozzáférhetünk a megfelelő indexek használatával, például az arguments[0]
visszaadja az első paramétert, példánkban ez a bar
. Ennek megfelelően egyszerűen végigmehetünk az összes argumentumon:
function foo() {
var i, l = arguments.length;
for (i = 0; i < l; i += 1) {
alert(arguments[i]);
}
}
Amennyiben az argumentumokat tömbként szeretnénk kezelni, egyszerűen átalakíthatjuk, így ezek után a tömb függvényeit már tudjuk használni:
function foo() {
var args = Array.prototype.slice.call(arguments); // az args már egy tömb.
}
Ezen kívül az arguments
objektum még rendelkezik két tulajdonsággal. Az arguments.callee
egy hivatkozás az éppen futó függvényre, valamint az arguments.caller
egy hivatkozás a függvényt meghívó függvényre. Ez utóbbi kettő használata kerülendő, ugyanis az arguments.caller
már a JavaScript 1.3 óta elavult, az arguments.callee
pedig nem használható strict mode esetén.
A magasabb rendű függvények (higher order function) olyan függvények, melyek más függvényekkel dolgoznak, azaz vagy paraméterként várják őket, vagy függvényeket adnak vissza.
A ECMAScript 5 tömb metódusainak egy része egy függvényt vár paraméternek (map
, filter
, reduce
, reduceRight
, forEach
, every
, some
), de ilyen már régen a nyelv részét képező sort
is. Ezek jellemzően egy függvényt várnak első paraméternek, és a tömb elemein futtatják le ezt a függvényt. Például a tömb sort
metódusa egy opcionális függvényt vár paraméternek, ami pozitív, negatív, illetve 0 értékkel tér vissza, attól függően, hogy a két összehasonlított érték közül az elsőt nagyobbnak, kisebbnek illetve egyenlőnek tekintjük a másikkal. Például:
var array = [1, 2, 3];
// Rendezzük fordított sorrendben
array.sort(function (a, b) {
if (a < b) {
return 1;
}
else if (a > b) {
return -1;
}
else {
return 0;
}
});
console.log(array); // [3, 2, 1]
Magunk is létrehozhatunk hasonló függvényeket:
function calculate(func, a, b) {
return func(a, b);
}
function add(a, b) {
returm a + b;
}
function multiply(a, b) {
return a * b;
}
calculate(multiply, 5, 3); // 15
calculate(add, 5, 3); // 8
Amennyiben ECMAScript 5-öt nem ismerő környezetben akarunk végrehajtani a tömb összes elemén egy függvényt, akkor azt megtehetjük például a következőképpen:
function each(array, func) {
var i, l = array.length;
for (i = 0; i < l; i += 1) {
func(array[i]);
}
}
// Írjuk ki a tömb összes elemét, egyesével:
each([1, 2, 3], console.log);
Amennyiben a tömbből egy új tömböt akarunk generálni, használhatjuk például a map
függvényt:
function map(array, func) {
var i, l = array.length, retArray = [];
for (i = 0; i < l; i += 1) {
// Minden elemre lefut a megkapott függvény,
// aminek eredményével feltöltjük a tömböt.
retArray[i] = func(array[i]);
}
return retArray;
}
map([1, 2, 3], function (a) { return a + 1 }); // [2, 3, 4]
A nyelvben persze más függvények is tekinthetők magasabb rendűnek, ugyanis például a setTimeout
és setInterval
is egy függvényt vár első paraméternek. A stringek replace
metódusa is opcionálisan tud függvényt fogadni második paraméterként.
Az is előfordulhat, hogy a függvény paramétereitől függően más függvényt szeretnénk használni, vagy generálni. Például több számhoz ugyanazt a számot szeretnénk hozzáadni, létrehozhatunk egy függvényt, ami ezt megteszi:
function addNumber(num) {
return function (a) {
// Closure miatt hozzáférünk a num változóhoz.
return a + num;
}
}
// az add10 függvény minden számhoz, amit paraméterként kap, 10-et ad hozzá.
var add10 = addNumber(10);
map([1, 2, 3], add10); // [11, 12, 13]
// Változó bevezetése nélkül, most 100-at adunk hozzá
map([1, 2, 3], addNumber(100)); // [101, 102, 103]
Magasabb rendű függvényekkel kapcsolatban érdemes elolvasni a következőket:
- Eloquent JavaScript - Functional Programming
- Higher-order programming in JavaScript
- Higher Order Javascript
Mint láthattuk, a JavaScript nyelvben a függvények működése és használata igencsak összetett tud lenni. Éppen ezért minden esetben gondoljuk át, mikor, milyen formáját használjuk, valamint azt is, hogy milyen környezetben hozzuk létre. Talán kicsit sok is a buktató, viszont egy szépen felépített struktúra sokat tud hozzátenni az áttekinthetőséghez. Remélem ezzel a cikkel sikerült kicsit rendet tenni a fejekben, és többen elkezdenek elmerülni a JavaScript függvények szépségében. További olvasnivalót adhat a témában az MDC és Dmitry A. Soshnikov oldala.