Skip to content

Instantly share code, notes, and snippets.

@Poetro

Poetro/javascript-fuggvenyek.md Secret

Created Mar 11, 2011
Embed
What would you like to do?
JavaScript függvények

JavaScript függvények

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.

Függvények létrehozása

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.

Függvény deklaráció

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:

  1. A függvény deklarációnak kötelezően van neve,
  2. vagy Program szinten, vagy egy másik függvény törzsében szerepel,
  3. akkor jön létre, amikor a program futása abba a környezetbe jut,
  4. 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.

Függvénykifejezés

A függvénykifejezés (function expression) létrehozásának szabályai a következők:

  1. a kódban ott szerepelhet, ahol kifejezés szerepelhet,
  2. neve elhagyható,
  3. nem módosítja a környezeti változók listáját,
  4. 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);
      }
    };

Azonnal meghívott függvénykifejezés

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:

  1. Létrehozunk egy függvénykifejezést, aminek foo a neve, de kívülről nem elérhető.
  2. A függvénykifejezést meghívjuk 'foobar' paraméterrel.
  3. 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.

Miért hasznosak a függvénykifejezések?

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é.

JScript és a függvénykifejezések

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

Function konstruktor

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.

Miért nem ajánlott használni?

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

return utasítás

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;

Closure

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.

this

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.

call és apply

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

strict mode

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.

arguments objektum

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.

Magasabb rendű függvények

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.

Függvényt fogadó függvények

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.

Függvényt visszaadó függvények

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:

Összefoglalás

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment