Blog webdeveloperski Patryk yarpo Jar

What does `this’ mean?

Autor wiadomości Listopad 6, 2011

JavaScript to bardzo przyjazny język. Tak - to bardzo przyjazny język, kiedy już się wie, jak nietypowym językiem jest. Nie bez powodu uznawany jest za najbardziej niezrozumiany język świata. Przyczyn tego stanu jest wiele. W tym wpisie postaram się skupić na jednej z nich - operator `this' i jego zmienność :).

Na początek

Jeśli w trakcie artykułu stwierdzisz, że warto byłoby uzupełnić wiedzę o JavaScript, aby dogłębniej zrozumieć treść polecam JavaScript na poważnie, szczególnie rozdziały 1.4.7 i 1.4.8.

Kontekst funkcji

Słowo kluczowe "this" wskazuje kontekst wywołania funkcji. Prosty przykład powinien wiele wyjaśnić:

var obj = {
   value : 'hello world',
   talk : function()
   {
       alert(this.value);
   }
};
obj.talk(); // wyświetli "hello world"

W metodzie `talk' operator `this' wskazywał na obiekt `obj'. Tak więc kontekstem wywołania metody `obj.talk' (jeśli wywołamy ją w sposób pokazany na przykładzie) jest obiekt `obj'.

`this' === `window'

Obiektem globalnym w każdym skrypcie uruchomionym w przeglądarce jest `window'. Tradycyjnie więcej powie kod:

function example()
{
   this.value = 'hello world';
}
window.example();
alert(value); // 'hello world'
alert(window.value); // 'hello world'
alert(window.value === value); // true
alert(this === window); // true

Zmienna `value' oraz `window.value' (to ta sama zmienna) mają wartość "hello world", która została przypisana do zmiennej `this.value' w metodzie `example' wywołanej jako `window.example'.... No tak - a miało być prosto! 🙂

Spokojnie. Kilka prostych zasad i to wszystko będzie zrozumiałe :). Zasady te odnoszą się do zwykłego trybu. W trybie strict mode nie muszą one obowiązywać (większość skryptów - sądzę, że ponad 90% nie jest w trybie strict mode).

Zasady:

każda zmienna (funkcja to też zmienna [przechowująca referencję na funkcję]) w globalnej przestrzeni nazw może być wywołana na dwa sposoby:

nazwa_zmiennej = xxx;

albo

window.nazwa_zmiennej = xxx;

W funkcji, która przy wywołaniu nie zostanie powiązana z konkretnym kontekstem (inaczej mówiąc `this' w tej funkcji nie przyjmie odpowiedniej wartości), operator `this' posiada wartość `window', czyli wskazuje na globalny kontekst. Stąd przypisanie w metodzie `example' jakiegokolwiek pola do `this' tak naprawdę przypisuje je do `window'.

Operator `new'

Operator `new' tworzy instancję typu obiektu zdefiniowanego przez użytkownika lub jednego z wbudowanych typów obiektów, który posiada funkcję konstruktora (źródło: developer.mozilla.org).

Spróbujmy zatem uruchomić poprzedni kod z wykorzystaniem tego operatora:

function Example()
{
   this.value = 'hello world';
};
new Example();
alert(value); // 'hello world'
alert(window.value); // 'hello world'
alert(window.value === value); // true
alert(this === window); // true

W wyniku działania takiego kodu otrzymamy komunikat (w konsoli firebuga):

ReferenceError: value is not defined

Jak sądzisz, dlaczego tak się stało?

Otóż wywołanie funkcji `Example' (mogłoby być także `window.Example') z wykorzystaniem operatora `new' sprawia, że `this' nie jest już w tej funkcji wiązane z `window'. Przez co przypisanie do niego pola `value' nie dodaje zmiennej do globalnej przestrzeni nazw.

Dlaczego `Example', a nie `example'?

Jest to konwencja proponowana przez D. Crockforda (m. in. autora książki Mocne strony JavaScript, czy formatu JSON). Jeśli metoda jest konstruktorem obiektu i do poprawnego działania wymaga użycia operatora `new' jej nazwa powinna zaczynać się wielką literą.

Jak pokazał powyższy przykład operatory `this' i `new' potrafią być bardzo kłopotliwe. Stąd też istnieją głosy, aby pisać swoje skrypty w taki sposób by nie wymagały stosowania operatora `new':

Ja od siebie zwykłem dodawać średnik za klamrą zamykającą konstruktor obiektu:

"Operator" self

Często zdarza się, że w kodzie będziemy mieli zagnieżdżone funkcje, np. tak:

var obj = {
    value : 'hello world',
    talk : function()
    {
        // this === obj
        setTimeout(function()
        {
            // this === window
            alert(this.value); // undefined!
        }, 1000);
    }
};

Zmienna `this.value' zwróci wartość `undefined', ponieważ zagnieżdżona funkcja posiada inny kontekst wywołania. Takich sytuacji w JS jest niezwykle wiele - szczególnie w przypadku wykorzystywania callbacków w Ajaksie.

Jednym z popularniejszych rozwiązań jest przypisanie wartości `this' do zmiennej:

var obj = {
    value : 'hello world',
    talk : function()
    {
        // this === obj
        var self = this;
        setTimeout(function()
        {
            // this === window
            // self === obj
            alert(self.value); // 'hello world'
        }, 1000);
    }
};

W powyższym przykładzie wykorzystałem mechanizm domknięć:

Warto pamiętać, że nie można usunąć, ani nadpisać `this'. Można natomiast przypisać jego wartość do innej zmiennej, a także można zmieniać poszczególne pola tego obiektu.

Wybrana nazwa dla "operatora" (`self') może być oczywiście inna - jest to zwykła zmienna. Najczęściej jednak występują "self", "that", "_this".

Ręczne ustawianie wartości `this'

To co za chwilę przeczytasz może zmienić Twoje życie. Pamiętaj, że nie będzie już odwrotu... Podczas wywoływania metody można określać w jakim kontekście ma zostać uruchomiona!

Metody `apply' i `call'

Jak wcześniej wspomniałem można dowolnie sterować kontekstem wywołania metody w JavaScript. Tradycyjnie podeprę się przykładowym kodem:

var x = {
    value : 'hello world',
    talk : function()
    {
        alert('jestem metodą x.talk i mówię: ' + this.value);
    }
};

var y = {
    value : 'Good bye',
    talk : function()
    {
        alert('jestem metodą y.talk i mówię: ' + this.value);
    }
};
x.talk(); // jestem metodą x.talk i mówię: hello world
y.talk(); // jestem metodą y.talk i mówię: good bye

Powyższy kod działa w pełni tak, jakbyśmy tego oczekiwali. Teraz jednak sprawię, aby metoda `x.talk' mówiła "good bye":

// wcześniejszy kod
x.talk.apply(y); // jestem metodą x.talk i mówię: good bye

Cóż się tu stało? Wywołanie funkcji poprzez `nazwa_funkcji.apply(kontekst wywołania)' powoduje, że wewnątrz tej funkcji `this' zostaje związany z przekazanym obiektem.

Co więcej, nie musi być to nawet obiekt:

function example()
{
    alert(this + 3);
}
example.call(5); // 8
example.call(110); //113

Różnica między `apply' i `call'

Metody `apply' i `call' działają tak samo. Jedyna różnica polega na przekazywanych parametrach:

fun.apply(kontekst [, tablica argumentów])

 

fun.call(kontekst [, argument1 [, argument2 [, ... [, argumentN]]]])

Jeśli wywoływana metoda nie posiada żadnych parametrów - nie ma różnicy, której metody użyjesz. Jeśli posiada parametry należy wykorzystać tę metodę, która w danej sytuacji jest bardziej przydatna - jeśli masz akurat tablicę (lub array-like object) wykorzystaj `apply', w przeciwnym wypadku `call' wydaje się lepszym rozwiązaniem.

W najnowszych wersjach JavaScript pojawiała się dodatkowo metoda `bind':

Zdarzenia

Jeszcze jednym ciekawym zagadnieniem jest zachowanie operatora `this' w przypadku funkcji obsługi zdarzeń. TODO

Warto przeczytać:

Komentarze (0) Trackbacks (0)

Brak komentarzy.


Leave a comment

 

Brak trackbacków.