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: this

Warsztat / Artykuły i tutoriale

JavaScript: this

Rafał Kukawski 1 listopada 2012 komentarze ()

Tagi:JavaScript

W JavaScript istnieje sobie magiczne słowo kluczowe this. Magiczne z tego powodu, że ma pewne cechy zmiennych, dostępne jest zawsze i w każdym miejscu, choć nigdzie go jawnie nie deklarujemy. Wartość this jest ustalana dynamicznie w momencie wywołania funkcji, dlatego na początku problematyczne może być zrozumienie skąd się dana wartość wzięła. Dodatkowo sprawę komplikuje tryb ścisły zdefiniowany w specyfikacji Ecmascript 5, który opisuje inne zachowanie JavaScriptu w ustalaniu wartości dla this. Ale jak się okaże, tryb ścisły znacznie upraszcza sprawę.

Zacznijmy od tego, że wywołaniu każdej funkcji towarzyszy kontekst wywołania. W kontekście wywołania znajdują się wszelkie informacje niezbędne do wykonania funkcji, takie jak spis wszystkich zmiennych i funkcji zadeklarowanych wewnątrz wywołanej funkcji, łańcuch zasięgów dzięki któremu możemy odwoływać się do zmiennych zadeklarowanych w funkcjach nadrzędnych i kontekście globalnym oraz w końcu tytułowe this, które wskazuje na obiekt (wyjątkiem jest strict mode) na rzecz którego została wywołana funkcja.

Przestrzeń globalna

Z this najczęściej będziemy mieli styczność operując na funkcjach, ale należy zaznaczyć, że w globalnym kontekście też mamy do niego dostęp. Wartość this w globalnym kontekście wskazuje na obiekt globalny.

Żeby potwierdzić te słowa, wykonamy prosty test. Pamiętamy, że w przeglądarkowej implementacji JavaScriptu dostęp do obiektu globalnego mamy przez window. Porównajmy go zatem z this.

Kod: Zaznacz cały
this === window; // true

this a funkcje

Sprawa this w funkcjach jest bardziej rozbudowana. Funkcje mogą być „samodzielne”, mogą też być zdefiniowane jako metody obiektu.

Kod: Zaznacz cały
function samodzielna () {
    return this;
}

var obiekt = {
    metoda: function () {
        return this;
    }
};

Wywołując powyższe funkcje uzyskamy

Kod: Zaznacz cały
samodzielna() === window; // true
obiekt.metoda() === window; // false
obiekt.metoda() === obiekt; // true

Trzecia linia przykładu pokazuje, że wywołując metodę obiektu, ten obiekt staje się kontekstem dla funkcji.

No dobrze, utwórzmy kolejny obiekt i przepiszmy do niego funkcję. W końcu JavaScript tego nie zabrania.

Kod: Zaznacz cały
function samodzielna () {
    return this;
}

var obiekt = {
    metoda: samodzielna
};

obiekt.metoda() === window; // false
obiekt.metoda() === obiekt; // true

lub

Kod: Zaznacz cały
var obiekt = {
    metoda: function () {
        return this;
    }
};

var drugi_obiekt = {
    metoda: obiekt.metoda,
};

drugi_obiekt.metoda() === obiekt; // false
drugi_obiekt.metoda() === drugi_obient; // true

Powyższe listingi potwierdzają twierdzenie ze wstępu, że wartość this jest ustalana dynamicznie, podczas wywołania funkcji i nie jest w żaden sposób powiązana z miejscem, gdzie została zadeklarowana.

Zgadywanka

Jak zatem zapamiętać co będzie wartością this? Najprościej jest popatrzeć jak funkcja jest wywoływana, a w szczególności co znajduje się bezpośrednio przed nazwą funkcji. Jeśli znajdziemy tam „ścieżkę” z użyciem operatora kropki, ostatnia nazwa przed nazwą funkcji stanie się thisem.

Kod: Zaznacz cały
foo.metoda(); // foo będzie kontekstem
foo.bar.baz.test(); // baz będzie kontekstem

Jeśli wywołanie funkcji nie będzie poprzedzone żadną ścieżką, kontekstem stanie się obiekt globalny lub — w przypadku trybu ścisłego — kontekst będzie niezdefiniowany.

Kod: Zaznacz cały
function foo () {
    return this;
}

function bar () {
    "use strict";
    return this;
}
foo(); // obiekt globalny jako kontekst
bar(); // undefined

Zmiana w zachowaniu w trybie ścisłym pozwala uniknąć popełniania trudnych do wykrycia błędów, gdzie pewne wartości mogą wyciekać do kontekstu globalnego.

Kod: Zaznacz cały
var foo = {
    bar: function (result) {
        this.baz = result;
    }
};

function test (callback) {
    callback(Math.random());
}

test(foo.bar);
typeof foo.baz; // "undefined"
typeof window.baz; // "number"

W trybie ścisłym, powyższy kod generowałby błędy.

Kod: Zaznacz cały
var foo = {
    bar: function (result) {
        "use strict";
        this.baz = result; // błąd, ponieważ this jest undefined
    }
};

function test (callback) {
    callback(Math.random());
}

test(foo.bar);

Funkcje konstruktory

Pewne odstępstwo od powyższych zasad stanowią funkcje-konstruktory, które wywołujemy z użyciem operatora new.

Kod: Zaznacz cały
var Foo = function () {
    this.bar = "baz"; // na co wskazuje ten this?
};

var foo = new Foo();
console.log(foo.bar); // "baz"
console.log(window.bar); // undefined

Gdybysmy funkcję Foo z powyższego przykładu wywołali bez użycia new, zgodnie z omówionymi wyżej regułami, this wskazywałby na obiekt globalny.

Kod: Zaznacz cały
Foo();
console.log(window.bar); // "baz"

Jednak operator new zmienia reguły. Operator ten tworzy nowy obiekt, który dziedziczy z prototypu Foo, zaś funkcja Foo zostanie wywołana z this ustawionym na ten utworzony obiekt.

Kod: Zaznacz cały
var foo = new Foo();
console.log(window.bar); // globalna przestrzeń nie zostaje zaśmiecona
console.log(foo.bar); // własność bar zostanie utworzona na obiekcie przekazanym przez operator new jako this to Foo

Modyfikacja this

JavaScript już nieraz udowodnił, że jest bardzo elastycznym językiem. Nie inaczej jest w przypadku ustalania this. Dzięki metodom call oraz apply obiektu funkcyjnego, można wywołać daną funkcję z dowolnym this.

Kod: Zaznacz cały
var foo = {
    bar: "lorem ipsum"
};

function test () {
    return this.bar;
}

test(); // undefined
test.call(foo); // "lorem ipsum"
test.apply(foo); // "lorem ipsum"

Wywołanie test() jest analogiczne do wywołania test.call(window).

Dzięki tej właściwości języka, można metody jednego typu obiektów zapożyczać do innych. Za przykład niech posłuży sposób w jaki można sprawdzić, czy zmienna jest tablicą, który działa na obiektach pochodzących z dowolnego źródła (np. innej ramki) lub rzutowanie obiektu arguments do tablicy.

Kod: Zaznacz cały
Object.prototype.toString.call(maybeArray) === "[object Array]";
var array = Array.prototype.slice.call(arguments, 0);

this zawsze obiektem?

Specyfikacja ES3 definiuje, że this zawsze będzie obiektem, zaś przekazanie undefined lub null powoduje przyjęcie obiektu globalnego. Sprawdźmy drugą cechę.

Kod: Zaznacz cały
function test () {
    return this;
}

test() === window; // true
test.call() === window; // true
test.call(undefined) === window; // true
test.call(null) === window; // true

Aby potwierdzić pierwsze stwierdzenie, przekażemy wartości prymitywne jako this.

Kod: Zaznacz cały
typeof test.call(true); // "object"
typeof test.call(3.14); // "object"
typeof test.call("lorem ipsum"); // "object"

Może nasuwać się pytanie, co się dzieje z przekazaną wartością? Jest ona rzutowana do obiektu w identyczny sposób jakbyśmy wywołali funkcję Object.

Kod: Zaznacz cały
true -> Object(true) -> new Boolean(true)
3.14 -> Object(3.14) -> new Number(3.14)
"lorem ipsum" -> Object("lorem ipsum") -> new String("lorem ipsum")

Powyższe zasady łamie tryb ścisły w ES5. Po pierwsze, obiekt globalny nie jest nigdy domyślnie przyjmowany.

Kod: Zaznacz cały
function test () {
    "use strict";
    return this;
}

test(); // undefined
test.call(); // undefined
test.call(undefined); // undefined
test.call(null); // null

Po drugie, wartości prymitywne nie będą rzutowane do obiektu.

Kod: Zaznacz cały
typeof test.call(true); // "boolean"
typeof test.call(3.14); // "number"
typeof test.call("lorem ipsum"); // "string"

Zablokowanie this

Wraz z ES5 funkcje zyskały nową metodę o nazwie bind. Za jej pomocą można ustalić „na stałe” this dla danej funkcji.

Kod: Zaznacz cały
function test () {
    return this;
}

var bound = test.bind(document);
test() === window; // true
bound() === window; // false
bound() === document; // true
bound.call(window) === window; // false
bound.call(window) === document; // true

Jeśli chcielibyśmy korzystać z bind w przeglądarkach, które tej metody nie posiadają, można podstawową funkcjonalność emulować za pomocą poniższego kodu.

Kod: Zaznacz cały
(function () {
    var slice = Array.prototype.slice;

    if (!Function.prototype.bind) {
        Function.prototype.bind = function (thisArg) {
            var fn = this;
            var args = slice.call(arguments, 1);

            return function () {
                return fn.apply(thisArg, args.concat(slice.call(arguments, 0)));
            };
        };
    }
}());

var foo = {bar: "baz"};
function () { return this.bar; }.bind(foo).call({another: "object"}); // "baz"

Funkcje obsługujące zdarzenia

Na dodatkową uwagę zasługują funkcje, które rejestrujemy w celu obsługi pewnych zdarzeń. Tych funkcji nie wywołujemy manualnie, tylko robi to za nas przeglądarka, gdy nastąpi dane zdarzenie. Dlatego to na przeglądarce spoczywa zadanie ustalenia this dla wywołania. A w tym przypadku this będzie wskazywał na obiekt na którym zarejestrowaliśmy funkcję. W poniższym przykładzie obiektem tym będzie document.

Kod: Zaznacz cały
function clickHandler () {
    console.log(this === document);
}
document.addEventListener("click", clickhandler, false);
Powyższe jest oczywiście zależne od przeglądarki. Przykładowo w starszych Internet Explorerach, funkcje zarejestrowane przez attachEvent miały this wskazujący na obiekt globalny. Po szczegóły odsyłam do artykułu Obsługa zdarzeń w przeglądarkach.

Podsumowanie

Jak widać, temat this jest dość rozbudowany. Ale z czasem i nabytą wprawą w JavaScripcie sprawa stanie się banalnie prosta. Wystarczy trzymać się jednej zasady — nieważne, gdzie funkcja została zadeklarowana. Ważne w jaki sposób została wywołana.

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