Ta strona używa ciasteczek (cookies), dzięki którym nasz serwis może działać lepiej. Dowiedz się więcej OK, rozumiem
WebHelp.pl Warsztat Artykuły JavaScript: Zmienne i funkcje

Warsztat / Artykuły i tutoriale

JavaScript: Zmienne i funkcje

Rafał Kukawski 24 lutego 2014 komentarze ()

Tagi:JavaScript

W tym artykule postanowiłem omówić temat zmiennych i funkcji w JavaScript. Temat jest dość rozbudowany i miejscami może przyprawiać o zawrót głowy, dlatego postaram się jak najprościej ogarnąć całość bez wprowadzania nadmiernego chaosu.

Deklaracja zmiennych i funkcji

Rozważania na temat zmiennych i funkcji w JavaScript rozpocznijmy od składni deklaracji.

Zmienne deklarujemy z użyciem słowa kluczowego var.

Kod: Zaznacz cały
var zmienna;

Opcjonalnie można do zmiennej przypisać domyślną wartość.

Kod: Zaznacz cały
var pi = 3.14;

A jeśli mamy potrzebę zadeklarowania więcej niż jednej zmiennej, można to zrobić jedną deklaracją.

Kod: Zaznacz cały
var prawda = true,
    jeden = 1,
    pi = 3.14,
    foo = "bar",
    bezWartosci;

Zmienna bez przypisanej wartości jest typu niezdefiniowanego (undefined).

Kod: Zaznacz cały
typeof bezWartosci; // "undefined"
typeof pi; // "number"
typeof foo; // "string"
typeof prawda; // "boolean"

Funkcje deklarujemy z użyciem słowa kluczowego function, po którym podajemy nazwę, dalej w nawiasach okrągłych wymieniamy opcjonalną listę argumentów, następnie w nawiasach klamrowych budujemy ciało funkcji, które może zawierać praktycznie dowolne instrukcje języka JavaScript.

Kod: Zaznacz cały
function sinh (arg) {
    return (Math.exp(arg) - Math.exp(-arg)) / 2;
}

Tak zadeklarowaną funkcję można potem wywołać używając nazwy sinh.

Kod: Zaznacz cały
sinh(0);

Ponadto, JavaScript oferuje notację zwaną wyrażeniem funkcyjnym

Kod: Zaznacz cały
var sinh = function (arg) {
    return (Math.exp(arg) - Math.exp(-arg)) / 2;
};

Math.sinh = function (arg) {
    return (Math.exp(arg) - Math.exp(-arg)) / 2;
};

sinh(0); // wywołanie
Math.sinh(0); // wywołanie

O różnicach w porównaniu z deklaracją funkcji napiszę później.

Argumenty funkcji

Definiując funkcję, można zaznaczyć jej kluczowe argumenty nadając im nazwę.

Kod: Zaznacz cały
function test (a, b, c, d) {}

Nazwanie argumentów nie ogranicza jednak możliwości wywołania funkcji z mniejszą lub większą ich ilością. Całkowicie poprawne i dozwolone są poniższe wywołania.

Kod: Zaznacz cały
test(1, 2, 3, 4); // komplet argumentów
test();
test(1);
test(1, 2);
test(1, 2, 3);
test(1, 2, 3, 4, 5, 6, 7, 8, 9); // więcej argumentów niż definiuje sygnatura funkcji

Brakujące argumenty będą typu niezdefiniowanego, zaś dostęp do nadmiarowych (i wszystkich innych) zapewni obiekt arguments.

Obiekt arguments

Każda funkcja ma powiązany ze sobą magiczny obiekt, do którego możemy odwołać się przez nazwę arguments.

Kod: Zaznacz cały
function test () {
    console.log(typeof arguments); // "object"
}

test(1, 2, 3, 4);

Obiekt ten indeksuje wszystkie parametry przekazane do funkcji, nawet te nadmiarowe.

Kod: Zaznacz cały
function test (a, b, c) {
    console.log(
        arguments[0],
        arguments[1],
        arguments[2],
        arguments[3],
        arguments[4]
    ); // 1, 2, 3, 4, 5
}

test(1, 2, 3, 4, 5);

Własność length tego obiektu informuje nas ile parametrów zostało przekazanych do funkcji. Dzięki niej można tworzyć funkcje o nieokreślonej z góry liczbie argumentów.

Kod: Zaznacz cały
function sum () {
    var sum = 0;

    for (var i = 0; i < arguments.length; i++) {
        sum += arguments[i];
    }

    return sum;
}

sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); // 55

Ciało funkcji

Wewnątrz funkcji możemy wstawiać dosłownie dowolne instrukcje języka JavaScript, włączając w to kolejne deklaracje funkcji. Tak, w JavaScript można zagnieżdżać funkcje, co jest bardzo ważną cechą tego języka.

Kod: Zaznacz cały
function outer () {
    function inner () {
        return "ipsum";
    }
    
    return "lorem " + inner();
}

outer(); // "lorem ipsum"

Warto jednak zapamiętać, że deklaracji funkcji nie można zagnieżdżać wewnątrz innych konstrukcji językowych, np. w blokach.

Kod: Zaznacz cały
function outer () {
    // <-- w tym miejscu można zagnieździć funkcję
    
    if (true) {
        // blok instrukcji warunkowej
        // w tym miejscu nie można zagnieździć funkcji
        function inner () {} // źle
    }
    
    // <-- tutaj również można zagnieździć funkcję
        
    for (var i = 0; i < 10; i++) {
        function inner2 () {} // źle
    }
}

Niestety, o tym fakcie poinformuje tylko tryb ścisły rzucając stosownym błędem. W trybie zwykłym przeglądarki starają się tę sytuację obsłużyć po swojemu, co skutkuje niestety różnym zachowaniem w poszczególnych browserach.

Kod: Zaznacz cały
function outer () {
    "use strict";
    
    if (true) {
        function inner () {}
    }
}
SyntaxError: in strict mode code, functions may be declared only at top level or immediately within another function

Zasięg zmiennych (i funkcji)

W JavaScript mamy do czynienia z dwoma zasięgami – globalnym oraz funkcyjnym (lokalnym). Oznacza to, że zmienna zadeklarowana poza funkcją jest zmienną globalną, do której mamy dostęp z dowolnego miejsca programu.

Kod: Zaznacz cały
var foo = "Hi!"; // zmienna globalna
    
function test () {
    foo = "Hello!"; // zmienna foo jest widoczna z każdego miejsca w kodzie
}
    
test();

Zmienna zadeklarowana wewnątrz funkcji jest zmienną lokalną, która jest tworzona gdy funkcja zostanie wywołana, zaś niszczona, gdy funkcja zakończy działanie i żaden kod nie wskazuje na tę zmienną. Takie zmienne nie są widoczne przez żaden kod znajdujący się „na zewnątrz” funkcji deklarującej tę zmienną, ale są widoczne przez wszystkie zagnieżdżone funkcje.

Kod: Zaznacz cały
var zero = 0; // zmienna globalna
    
function test () {
    var raz = 1;
        
    (function () {
        var dwa = 2;
    
        console.log(zero, raz, dwa); // z tego miejsca widzimy zmienne lokalne oraz zmienne nadrzędnych zasięgów
    }());
        
    console.log(zero, raz); // z tego miejsca widać tylko zmienne zero i raz. Zmienna dwa jest ukrywa wewnątrz zasięgu zagnieżdżonej funkcji
}
    
test();

Istnieje jeden przypadek przez który stracimy dostęp do zmiennych z nadrzędnych zasięgów – gdy utworzymy deklarację o nazwie występującej w nadrzędnych zasięgach.

Kod: Zaznacz cały
function foo () {
    var test = 1;
        
    (function () {
        var test = 2;
        console.log(test); // 2
        // zagnieżdżona funkcja traci dostęp do zmiennej `test` zadeklarowanej wewnątrz `foo`, bo sama deklaruje zmienną o tej samej nazwie
    }());
        
    console.log(test); // 1
}
    
foo();

Czytając teksty na temat JavaScriptu na pewno spotkasz się z pojęciem domknięcia (ang. Closure). Z domknięciem mamy do czynienia, gdy tworzymy funkcję. Pojęcie to oznacza właśnie dostęp funkcji zagnieżdżonej do zmiennych nadrzędnych zasięgów. Tutaj należy zauważyć, że funkcja utworzona w kodzie globalnym też tworzy domknięcie, ponieważ zyskuje dostęp do zmiennych kodu globalnego.

Funkcje jako wartości

Funkcje w JavaScript są obiektami. Od innych obiektów wyróżnia je cecha, że można je „wywołać”, ale nie zmienia to faktu, że mają typowe cechy obiektów. A to oznacza, że jak każdy inny obiekt, obiektami funkcyjnymi można żonglować w całym kodzie aplikacji. Można je przypisywać do zmiennych

Kod: Zaznacz cały
function test () { console.log("test"); }
var zmienna = test;
zmienna(); // "test"

można też przekazać jako wartość parametru innej funkcji

Kod: Zaznacz cały
var liczby = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
function sortujMalejaco (a, b) {
    return b - a;
}

console.log(liczby.sort(sortujMalejaco)); // [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]

Ponadto funkcja może zwrócić funkcję jako wynik działania, co jest kolejną potężną cechą JavaScriptu.

Kod: Zaznacz cały
function counter () {
    var count = 0;
        
    function increment () {
        return ++count;
    }
        
    return increment;
}
    
var nextInt = counter();
console.log(nextInt(), nextInt(), nextInt()); // 1, 2, 3

Jak widać w powyższym przykładzie, każde kolejne wywołanie nextInt zwraca liczbę większą o 1 od poprzedniego wywołania. W kodzie mamy dostęp do funkcji „liczącej” ale nie mamy dostępu do zmiennej, która przechowuje stan licznika. W ten sposób utworzyliśmy moduł, którego prywatne dane (jego stan) są trzymane poza zasięgiem innego kodu.

Żeby dobrze zrozumieć powyższy przykład, proszę zauważyć, że wywołanie funkcji counter tworzy zmienną lokalną count z początkową wartością 0 oraz obiekt funkcyjny increment, którego zadaniem jest zwiększanie wartości count o jeden. Obiekt funkcji zostaje zwrócony do kodu wywołującego counter. Zwrócony obiekt funkcyjny zostaje przypisany do zmiennej nextInt. Mimo, że funkcja counter zakończyła działanie, referencje count i increment nie zostały zniszczone, ponieważ na funkcję wskazuje zmienna nextInt a na zmienną count wskazuje ciało zwróconej funkcji. Te dane będą przechowywane w pamięci tak długo, jak długo zmienna nextInt (lub inna zmienna) będzie wskazywać na obiekt funkcji increment.

Wynoszenie zmiennych i funkcji

Przeanalizujmy taki krótki fragment kodu

Kod: Zaznacz cały
function test () {
    console.log(foo); // undefined
    
    var foo = "bar";
}
    
test();

W kodzie odwołujemy się w linii 2 do zmiennej foo, którą deklarujemy dopiero w linii 4. Mimo tego kod wykonuje się bez błędu, choć wynik może trochę zaskakiwać. W konsoli zobaczymy wartość undefined.

Podobnie sprawa wygląda z funkcjami.

Kod: Zaznacz cały
test();
    
function test () {
    console.log("magic!");
}

Możemy funkcję wywołać, mimo, że zostaje zadeklarowana później. Można też spróbować bardziej ekstremalny przypadek.

Kod: Zaznacz cały
function test () {
    console.log(foo); // undefined
    
    if (false) { // nigdy nie wejdziemy do tego bloku
        var foo = 1;
    }   
}
    
test();

Tutaj, mimo że zmienna deklarowana jest w nieosiągalnym bloku, kod wykonuje się bezbłędnie.

Mamy tutaj do czynienia z cechą zwaną wynoszeniem deklaracji zmiennych i funkcji. Innymi słowy, można sytuację traktować, jak gdyby silnik JavaScriptu zmodyfikował nasze kody do postaci:

Kod: Zaznacz cały
function test () {
    var foo;
        
    console.log(foo); // undefined
    
    foo = "bar";
}
    
test();

oraz

Kod: Zaznacz cały
function test () {
    console.log("magic!");
}
    
test();

oraz

Kod: Zaznacz cały
function test () {
    var foo;
        
    console.log(foo); // undefined
    
    if (false) { // nigdy nie wejdziemy do tego bloku
        foo = 1;
    }   
}
    
test();

JavaScript wykonuje funkcje dwu-etapowo. Najpierw przeprowadza analizę, której rezultatem jest „wyuczenie się” wszystkich zmiennych oraz funkcji, przy czym w przypadku funkcji wynoszona jest cała definicja, a w przypadku zmiennej tylko jej nazwa. Potem wykonane zostaną instrukcje zawarte w ciele funkcji w podanej w źródle kolejności. Stąd w konsoli zobaczymy wartość undefined, bo przypisanie wartości nastąpi zaraz potem.

Omawiana cecha jest przez niektórych uważana za wadę języka, zaś Douglas Crockford proponuje, żeby wszystkie zmienne, deklarować zaraz w pierwszej linii ciała funkcji, co pozwoli uniknąć niespodzianek.

Klasyczną „niespodziankę” zaprezentuje poniższy przykład.

Kod: Zaznacz cały
var callbacks = [];
    
for (var i = 0; i < 3; i++) {
    var index = i + 1;
        
    callbacks.push(function () {
        alert(index);
    });
}
    
for (var i = 0; i < 3; i++) {
    callbacks[i]();
}

Zamiast sekwencji 1, 2, 3 zobaczymy trzy razy liczbę 3. A wszystko przez to, że zmienna index – mimo 3 iteracji pętli for – zostaje utworzona tylko raz. Każda iteracja tylko modyfikuje jej wartość. W momencie wywołania funkcji, zmienna index ma wartość 3 i właśnie ta jest wyświetlona.

Z problemem można sobie poradzić na kilka sposobów. Klasyczny sposób, który zadziała nawet w Ecma-scriptcie 3, polega na utworzeniu samo-wywołującej się funkcji, która w swoim zasięgu przechowa bieżącą wartość zmiennej index:

Kod: Zaznacz cały
var callbacks = [];
    
for (var i = 0; i < 3; i++) {
    var index = i + 1;
        
    callbacks.push(function (arg) {
        return function () {
            alert(arg);
        };
    }(index)); // <-- wartość index zostanie przekazana do parametru arg
}
    
for (var i = 0; i < 3; i++) {
    callbacks[i]();
}

Żeby powyższa czynność była prostsza w wykonaniu, przyszłe wersje języka będą oferować również zasięg blokowy. Zmienne o tym zasięgu będziemy deklarować słowem kluczowym let zamiast var.

Kod: Zaznacz cały
var callbacks = [];
    
for (var i = 0; i < 3; i++) {
    let index = i + 1;
        
    callbacks.push(function () {
        alert(index);
    });
}
    
for (var i = 0; i < 3; i++) {
    callbacks[i]();
}

Deklaracja funkcji vs wyrażenie funkcyjne

W pierwszym rozdziale obiecałem wyjaśnić różnicę pomiędzy deklaracją funkcji a wyrażeniem funkcyjnym. Obydwie konstrukcje składniowe tworzą nowe obiekty funkcyjne. Główna różnica polega na tym, że funkcja zdefiniowana za pomocą wyrażenia funkcyjnego nie jest wynoszona do góry.

Kod: Zaznacz cały
foo(); // "ok"
bar(); // TypeError

function foo () { return "ok"; }
var bar = function () { return "ok"; }

Jak tłumaczyłem w poprzednim rozdziale, deklaracja funkcji foo zostaje wyniesiona w całości, zaś z kolejnej linii kodu wyniesiona zostaje tylko nazwa zmiennej bar. Funkcja zdefiniowana wyrażeniem funkcyjnym zostanie utworzona dopiero gdy nastąpi operacja przypisania z tej linii.

W związku z tym, że wyniesiona została nazwa bar, próba wywołania funkcji wygeneruje błąd TypeError a nie ReferenceError, ponieważ istnieje już referencja z nazwą bar.

Pozostaje kwestia rozróżnienia kiedy mamy do czynienia z deklaracją funkcji a kiedy z wyrażeniem funkcyjnym. Jak sama nazwa wskazuje, wyrażenia funkcyjnego użyjemy tylko w miejscach, gdzie składnia języka oczekuje wyrażenia. Przykładami są prawa strona operatora przypisania, przekazywanie parametrów do wywolywanej funkcji, itp. Deklaracja funkcji jest samodzielną konstrukcją składniową.

Funkcje anonimowe

Wszystkie podane w tym artykule przykłady użycia wyrażenia funkcyjnego tworzą funkcje bez nazwy. Takie funkcje w JavaScript nazywamy anonimowymi.

Nic nie stoi na przeszkodzie, żeby nazwać funkcję w wyrażeniu funkcyjnym.

Kod: Zaznacz cały
(function foo (ok) {
    if (ok) {
        foo();
    }
}(true));

Możliwość nazwania funkcji z wyrażenia funkcyjnego pozwala wywołać siebie rekurencyjnie.

Kod: Zaznacz cały
[1, 2, 3, 4, 5].map(function factorial (num) {
    return num > 0 ? num * factorial(num - 1) : 1;
});

Co prawda, można uzyskać rekurencję przez użycie arguments.callee

Kod: Zaznacz cały
[1, 2, 3, 4, 5].map(function (num) {
    return num > 0 ? num * arguments.callee(num - 1) : 1;
});

ale ostatnia wersja specyfikacji języka pozbyła się własności arguments.callee w trybie ścisłym. Korzystanie z tej własności utrudnia optymalizację kodu przez współczesne silniki JavaScriptu.

Ponadto, brak nazwy funkcji generuje jeden dość istotny problem – utrudnia debugowanie kodu, ponieważ debuger będzie pokazywał cały zbiór anonimowych funkcji w widoku stosu wywołań.

Współczesne debuggery starają się nieco temu problemowi zaradzić. Przykładowo, używają nazwy zmiennej, do której przypisana była anonimowa funkcja, co częściowo redukuje kłopot.

Jeden fakt należy zapamiętać nazywając funkcje w wyrażeniu funkcyjnym. Ich nazwa nie jest widoczna w zasięgu definiującym tę funkcję. Podana nazwa jest widoczna tylko wewnątrz tak zdefiniowanej funkcji.

Kod: Zaznacz cały
var f = function foo (ok) {
    console.log("foo");
    
    if (ok) {
        foo(); // w tym miejscu nazwa `foo` jest widoczna
    }
};

f(true);
foo(true); // ReferenceError: foo is not defined - jak widać, tutaj `foo` nie ma

Masz pytania lub wątpliwości? Odwiedź nasze forum dyskusyjne.

Rafał Kukawski

Programista, webmaster. Szczególnie upodobał sobie JavaScript i technologie klienckie, choć strona serwera i bazy danych nie stanowią tajemnicy. Tworzy też aplikacje na urządzenia mobilne. kukawski.pl.


Komentarze


HTML CSS JavaScript PHP bazy danych MySQL Flash grafika framework hosting domeny pozycjonowanie wordpress Facebook