JavaScript: sterowanie kontekstem wywołania funkcji (cz. I)

Autor: | 11 grudnia 2013|Blog, Techniczne|0 komentarzy|Wyświetlenia: 14113

JavaScript uchodzi za język trudny. Czasem zarzuca mu się wręcz działanie sprzeczne z logiką. Nie chcę szczególnie polemizować z takim stanowiskiem. Wolę w tym wpisie raczej pokazać kilka mechanizmów związanych z operatorem `this’, które można uznać za dziwaczne, jednak po poznaniu znacznie poszerzają wachlarz możliwości programisty.

Kontekst wywołania funkcji

Każda funkcja w JavaScript posiada dwa obiekty z nią skojarzone:

  • this – kontekst wywołania
  • arguments – (array-like object) lista argumentów przekazanych przy wywołaniu

Zobaczmy, jak wyglada to w praktyce:

function example1()
{
    console.log('this: ', this, ', arguments: ', arguments);
}
example1();

Fragmenty kodu umieszczone w tym artykułe można uruchamiać w konsoli przeglądarki. Konsola ta najczęściej dostępna jest po wcisnięciu F12.

W wyniku wywołania funkcji `example1’ otrzymamy w konsoli:

this: Window, arguments: []

Jak widać przy wywołaniu takiej funkcji bez parametrów `this’ przyjmuje wartość obiektu globalnego `window’, natomiast `arguments’ jest pustą listą.

Może właśnie zapytałeś: “Jeśli `this’ === `windows’, to czy zmienne globalne są właściwościami obiektu `window’?

Sprawdźmy:

function example2()
{
    window.console.log(this===window);
}
window.example2();

W wyniku wywołania funkcji `window.example2’ otrzymamy: `true’. Zatem – tak, zmienne globalne w skryptach uruchamianych w przeglądarce są właściwościami obiektu globalnego `window’. Domyślnym kontekstem dla skryptów uruchamianych w normalnym trybie w przeglądarkach jest właśnie `window’. Sprawę trochę zmienia (porządkuje) tryb ścisły, o którym nie będę pisał w tym poście. Jest to temat na osobny artykuł.

Operator `new’

Skoro już wiemy, że domyślnie `this’ wskazuje na obiekt globalny, to co należy zrobić, aby powiąząć `this’ z aktualnym obiektem?

rozwiązanie 1:

var obj = {
    x : 'jestem wartością obj.x',
    test : function() { console.log(this.x); }
};
obj.test(); // w konsoli pokaże się "jestem wartością obj.x"

rozwiązanie 2:

function Constructor()
{
    this.x = 'jestem wartością Constructor.x';
    this.test = function() { console.log(this.x); }
};
(new Constructor()).test(); // w konsoli pokaże się "jestem wartością Constructor.x"

W obu przypadkach zarówno pole `x’, jak i pole `test’ (będące referencją na funkcję) są publiczne. Można odwołać się:

obj.x;
(new Constructor()).x;

lub nawet nadpisać te wartości. JavaScript pozwala na to. Najczęściej jednak NIE należy tego robić.

A co by się stało, gdybyśmy wywołali konstruktor `Constructor’ bez operatora `new’? Wtedy funkcja ta zostałaby potraktowana jako zwykła funkcja, która nic nie zwraca! To z kolei wywołaloby błąd podczas wywołania jakiegokolwiek pola. Zmienne, do których przypisano `undefined’ nie posiadają pól. Jest to jedna z olbrzymich wad korzystania z operatorów `this’ i `new’. Jeśli nie korzystamy z trybu ścisłego (a większość stworzonego do tej pory kodu JavaScript zdecydowanie nie korzysta z trybu ścisłego), to w żaden sposób nie zostaniemy o tym poinformowani. Błędnie działajacą aplikacja będzie jedynym źrodłem informacji o błędnym kodzie.

Sterowanie kontekstem

A co, gdybym Ci powiedział, że możesz wywołać funkcje w wybranym przez Ciebie kontekście. Brzmi troche jak Matrix? Nie – to tylko programowanie w najpodlejszym i najpiękniejszym jednocześnie jezyku świata (czytaj – JavaScript).

Aby sterować kontekstem wywołania mamy dwie niemal bliźniacze metody:

  • function.apply(context, [arg1[, arg2[, …. [, argN]]])
  • function.call(context, arguments-array)

Jak je wykorzystać?

var x = 'window'
,   obj1 = {
        x : 'obj1',
        test : function() { console.log('obj1.test ' + this.x); }
},  obj2 = {
    x : 'obj2',
};

function test() { console.log('po prostu test ' + this.x); }

obj1.test();             // ‘obj1.test obj1’
obj1.test.apply(obj1);   // ‘obj1.test obj1’
obj1.test.apply(obj2);   // ‘obj1.test obj2’
test.apply(obj1);        // ‘po prostu test obj1’
test.apply(window);      // ‘po prostu test window’
obj1.test.apply(this);   // ‘obj1.test window’
test();                  // ‘po prostu test window’

Jak widać za każdym razem operator `this’ wewnątrz konkretnej funkcji był wiązany z innym obiektem. Co więcej – w funkcji globalnej `test’ mogliśmy wykorzystać `this’ (bez błędów przy kompilacji, bo takiej nie ma w JavaScript), choć na pierwszy rzut oka wydawało się, iż jest to bez sensu. Nie pojawi się także żaden komunikat w trakcie wykonania skryptu – parser uzna, że widocznie tak chciałeś, nawet gdy wygląda to bardzo podejrzanie.

Dla tego typu wywołań nie ma różnicy, czy użyjemy ‘apply’ czy ‘call’. Różnice można dostrzec, gdy do wywoływanej funkcji chcemy przekazać parametry, np.

example.apply(obj1, 1, 'dwa', {});
example.call(obj1, [1, 'dwa', {}]);

oba można “rozkodować” niejako jakby:

obj1.example(1,2,3);  // czyli wywołujemy example jakby była metoda obj1
                      // wiążemy operator `this’ tej funkcji z obiektem `obj1'
                      // oraz przekazujemy do niej trzy argumenty: 1, ‘dwa’, {} (obiekt).

Bardzo późne wiązanie

Problem wiązania operatora `this’ z odpowiednim kontekstem ma jeszcze kilka problemów. Chciałbym poruszyć jeden z nich, który wymusił dodanie trzeciej funkcji wiążącej funkcję z kontekstem jej wywołania – `bind’.

Załóżmy, że mamy obiekt identyczny jak w poprzednich przykładach:

var x = 'window'
,   obj1 = {
    x : 'obj1',
    test : function() { console.log('obj1.test ' + this.x); }
},  test;

test = obj1.test;   // przypisujemy referencję do funkcji `obj1.test'. Całkowicie prawidłowa składnia
test();             // ‘obj1.test window’ ?!

Co tu się dziwnego stało? Do zmiennej `test’ przypisaliśmy referencję do funkcji, po czym ja wywołaliśmy. Wszystko wydaje się być OK. Nie ma błędów w konsoli, a mimo to efekt nie jest tym, czego oczekiwaliśmy.

O zgrozo – metoda `test’ obiektu `obj1’ po przypisaniu do globalnej zmiennej `test’ będzie domyślnie uruchamiana w kontekście globalnym, a nie (jak każdy pewnie założył) w kontekście właściwego sobie obiektu.

Można to łatwo rozwiązać:

test = obj1.test;
test.apply(obj1); // ‘obj1.test obj1’
test.call(obj1);  // ‘obj1.test obj1’ jak widać w tym wypadku też brak różnic miedzy `call’ i `apply’

Czy jednak na pewno musimy za każdym razem wywoływać metody służące do wiązania z właściwym kontekstem? Nie. Możemy (w nowoczesnych przeglądarkach) zrobić tak:

test = obj1.test.bind(obj1);
test(); // 'obj1.test obj1'

A co ze starymi przeglądarkami?

można zrobić prostą łatę:

if (!Function.prototype.bind)
{
    Function.prototype.bind = function (oThis)
    {
        if (typeof this !== 'function')
        {
            throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
        }
        var aArgs = Array.prototype.slice.call(arguments, 1)
        ,   fToBind = this
        ,   fNOP = function () {}
        ,   fBound = function ()
            {
                return fToBind.apply(this instanceof fNOP && oThis
                    ? this
                    : oThis,
                    aArgs.concat(Array.prototype.slice.call(arguments)));
            };

        fNOP.prototype = this.prototype;
        fBound.prototype = new fNOP();

        return fBound;
    };
}

Źródło: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#Compatibility

Wielkość litery “F” we wszystkich odwołaniach się do `Function’ nie jest pomyłką. Odwołujemy się do obiektu `Function’, a nie do literału funkcyjnego.

Koniec części pierwszej

Dziękuję za poświęcony czas. Już niedługo zostanie opublikowany kolejny artykuł nieco mocniej eksplorujący świat operatora `this’ w JavaScript.

Autor: | 11 grudnia 2013|Blog, Techniczne|0 komentarzy|Wyświetlenia: 14113

O autorze:

Zostaw komentarz

JavaScript: sterowanie kontekstem wywołania funkcji (cz. I)

Autor: | 11 grudnia 2013|Blog|Możliwość komentowania JavaScript: sterowanie kontekstem wywołania funkcji (cz. I) została wyłączona|Wyświetlenia: 3914

JavaScript uchodzi za język trudny. Czasem zarzuca mu się wręcz działanie sprzeczne z logiką. Nie chcę szczególnie polemizować z takim stanowiskiem. Wolę w tym wpisie raczej pokazać kilka mechanizmów związanych z operatorem `this’, które można uznać za dziwaczne, jednak po poznaniu znacznie poszerzają wachlarz możliwości programisty.

Kontekst wywołania funkcji

Każda funkcja w JavaScript posiada dwa obiekty z nią skojarzone:

  • this – kontekst wywołania
  • arguments – (array-like object) lista argumentów przekazanych przy wywołaniu

Zobaczmy, jak wyglada to w praktyce:

function example1()
{
    console.log('this: ', this, ', arguments: ', arguments);
}
example1();

Fragmenty kodu umieszczone w tym artykułe można uruchamiać w konsoli przeglądarki. Konsola ta najczęściej dostępna jest po wcisnięciu F12.

W wyniku wywołania funkcji `example1’ otrzymamy w konsoli:

this: Window, arguments: []

Jak widać przy wywołaniu takiej funkcji bez parametrów `this’ przyjmuje wartość obiektu globalnego `window’, natomiast `arguments’ jest pustą listą.

Może właśnie zapytałeś: “Jeśli `this’ === `windows’, to czy zmienne globalne są właściwościami obiektu `window’?

Sprawdźmy:

function example2()
{
    window.console.log(this===window);
}
window.example2();

W wyniku wywołania funkcji `window.example2’ otrzymamy: `true’. Zatem – tak, zmienne globalne w skryptach uruchamianych w przeglądarce są właściwościami obiektu globalnego `window’. Domyślnym kontekstem dla skryptów uruchamianych w normalnym trybie w przeglądarkach jest właśnie `window’. Sprawę trochę zmienia (porządkuje) tryb ścisły, o którym nie będę pisał w tym poście. Jest to temat na osobny artykuł.

Operator `new’

Skoro już wiemy, że domyślnie `this’ wskazuje na obiekt globalny, to co należy zrobić, aby powiąząć `this’ z aktualnym obiektem?

rozwiązanie 1:

var obj = {
    x : 'jestem wartością obj.x',
    test : function() { console.log(this.x); }
};
obj.test(); // w konsoli pokaże się "jestem wartością obj.x"

rozwiązanie 2:

function Constructor()
{
    this.x = 'jestem wartością Constructor.x';
    this.test = function() { console.log(this.x); }
};
(new Constructor()).test(); // w konsoli pokaże się "jestem wartością Constructor.x"

W obu przypadkach zarówno pole `x’, jak i pole `test’ (będące referencją na funkcję) są publiczne. Można odwołać się:

obj.x;
(new Constructor()).x;

lub nawet nadpisać te wartości. JavaScript pozwala na to. Najczęściej jednak NIE należy tego robić.

A co by się stało, gdybyśmy wywołali konstruktor `Constructor’ bez operatora `new’? Wtedy funkcja ta zostałaby potraktowana jako zwykła funkcja, która nic nie zwraca! To z kolei wywołaloby błąd podczas wywołania jakiegokolwiek pola. Zmienne, do których przypisano `undefined’ nie posiadają pól. Jest to jedna z olbrzymich wad korzystania z operatorów `this’ i `new’. Jeśli nie korzystamy z trybu ścisłego (a większość stworzonego do tej pory kodu JavaScript zdecydowanie nie korzysta z trybu ścisłego), to w żaden sposób nie zostaniemy o tym poinformowani. Błędnie działajacą aplikacja będzie jedynym źrodłem informacji o błędnym kodzie.

Sterowanie kontekstem

A co, gdybym Ci powiedział, że możesz wywołać funkcje w wybranym przez Ciebie kontekście. Brzmi troche jak Matrix? Nie – to tylko programowanie w najpodlejszym i najpiękniejszym jednocześnie jezyku świata (czytaj – JavaScript).

Aby sterować kontekstem wywołania mamy dwie niemal bliźniacze metody:

  • function.apply(context, [arg1[, arg2[, …. [, argN]]])
  • function.call(context, arguments-array)

Jak je wykorzystać?

var x = 'window'
,   obj1 = {
        x : 'obj1',
        test : function() { console.log('obj1.test ' + this.x); }
},  obj2 = {
    x : 'obj2',
};

function test() { console.log('po prostu test ' + this.x); }

obj1.test();             // ‘obj1.test obj1’
obj1.test.apply(obj1);   // ‘obj1.test obj1’
obj1.test.apply(obj2);   // ‘obj1.test obj2’
test.apply(obj1);        // ‘po prostu test obj1’
test.apply(window);      // ‘po prostu test window’
obj1.test.apply(this);   // ‘obj1.test window’
test();                  // ‘po prostu test window’

Jak widać za każdym razem operator `this’ wewnątrz konkretnej funkcji był wiązany z innym obiektem. Co więcej – w funkcji globalnej `test’ mogliśmy wykorzystać `this’ (bez błędów przy kompilacji, bo takiej nie ma w JavaScript), choć na pierwszy rzut oka wydawało się, iż jest to bez sensu. Nie pojawi się także żaden komunikat w trakcie wykonania skryptu – parser uzna, że widocznie tak chciałeś, nawet gdy wygląda to bardzo podejrzanie.

Dla tego typu wywołań nie ma różnicy, czy użyjemy ‘apply’ czy ‘call’. Różnice można dostrzec, gdy do wywoływanej funkcji chcemy przekazać parametry, np.

example.apply(obj1, 1, 'dwa', {});
example.call(obj1, [1, 'dwa', {}]);

oba można “rozkodować” niejako jakby:

obj1.example(1,2,3);  // czyli wywołujemy example jakby była metoda obj1
                      // wiążemy operator `this’ tej funkcji z obiektem `obj1'
                      // oraz przekazujemy do niej trzy argumenty: 1, ‘dwa’, {} (obiekt).

Bardzo późne wiązanie

Problem wiązania operatora `this’ z odpowiednim kontekstem ma jeszcze kilka problemów. Chciałbym poruszyć jeden z nich, który wymusił dodanie trzeciej funkcji wiążącej funkcję z kontekstem jej wywołania – `bind’.

Załóżmy, że mamy obiekt identyczny jak w poprzednich przykładach:

var x = 'window'
,   obj1 = {
    x : 'obj1',
    test : function() { console.log('obj1.test ' + this.x); }
},  test;

test = obj1.test;   // przypisujemy referencję do funkcji `obj1.test'. Całkowicie prawidłowa składnia
test();             // ‘obj1.test window’ ?!

Co tu się dziwnego stało? Do zmiennej `test’ przypisaliśmy referencję do funkcji, po czym ja wywołaliśmy. Wszystko wydaje się być OK. Nie ma błędów w konsoli, a mimo to efekt nie jest tym, czego oczekiwaliśmy.

O zgrozo – metoda `test’ obiektu `obj1’ po przypisaniu do globalnej zmiennej `test’ będzie domyślnie uruchamiana w kontekście globalnym, a nie (jak każdy pewnie założył) w kontekście właściwego sobie obiektu.

Można to łatwo rozwiązać:

test = obj1.test;
test.apply(obj1); // ‘obj1.test obj1’
test.call(obj1);  // ‘obj1.test obj1’ jak widać w tym wypadku też brak różnic miedzy `call’ i `apply’

Czy jednak na pewno musimy za każdym razem wywoływać metody służące do wiązania z właściwym kontekstem? Nie. Możemy (w nowoczesnych przeglądarkach) zrobić tak:

test = obj1.test.bind(obj1);
test(); // 'obj1.test obj1'

A co ze starymi przeglądarkami?

można zrobić prostą łatę:

if (!Function.prototype.bind)
{
    Function.prototype.bind = function (oThis)
    {
        if (typeof this !== 'function')
        {
            throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
        }
        var aArgs = Array.prototype.slice.call(arguments, 1)
        ,   fToBind = this
        ,   fNOP = function () {}
        ,   fBound = function ()
            {
                return fToBind.apply(this instanceof fNOP && oThis
                    ? this
                    : oThis,
                    aArgs.concat(Array.prototype.slice.call(arguments)));
            };

        fNOP.prototype = this.prototype;
        fBound.prototype = new fNOP();

        return fBound;
    };
}

Źródło: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#Compatibility

Wielkość litery “F” we wszystkich odwołaniach się do `Function’ nie jest pomyłką. Odwołujemy się do obiektu `Function’, a nie do literału funkcyjnego.

Koniec części pierwszej

Dziękuję za poświęcony czas. Już niedługo zostanie opublikowany kolejny artykuł nieco mocniej eksplorujący świat operatora `this’ w JavaScript.

Autor: | 11 grudnia 2013|Blog|Możliwość komentowania JavaScript: sterowanie kontekstem wywołania funkcji (cz. I) została wyłączona|Wyświetlenia: 3914

O autorze: