Scope v ExtJS

Několik tipů jak porozumět scope v ExtJS. Kolikrát jste hledali problém a zjistili že za něj může právě scope. Tento článek popíše několik základních konstrukcí, ve kterých programátoři nejvíce chybují.

Co to vlastně scope je? Každá funkce v javascriptu je metodou objectu, i když zapíšete definici funkce takto:

function fn() {
    console.log('my fn');
}

Pokud není funkce definovaná uvnitř nějakého objectu, je vždy součástí window, což je pro Vás global cope. Funkci v tomto scope můžete volat třemi způsoby:

fn();
this.fn();
window.fn();

Z těchto tří volání je pro nás nejzajímavější právě volání this.fn(). Proč? Právě this se mění se scopem, ze kterého na něj přistupujeme. Jak jsme si již ukázali, global scope je window a v něm this používat nemusíme (protože jsme leniví) a také proto, že this je v tomto případě rovna právě window (a my jsme leniví psát window nebo vůbec netušíme že nějaké window je).

Nyní si vytvoříme dva jednoduché objekty.

var object1 = {
    variable: 1,
    fn: function() {
        console.log('object1: ' + this.variable);
    }
}
var object2 = {
    variable: 2,
    fn: function() {
        console.log('object2: ' + this.variable);
    }
}

Pokud nyní zavoláme námi již zmíněné:

fn();
this.fn();
window.fn();

dostaneme chybu - fn is not defined. Jednoduché. Object window = this nemá v tuto chvíli nadefinovanou funkci fn. Zavoláme tedy:

object1.fn();
object2.fn();

a výsledek je o poznání lepší. Volaná funkce přistupuje vždy k proměnné variable, která je nadefinována v rámci daného objektu. Jednoduché? Situaci si trošku zkomplikujeme:

object1.fn.call(object2);

Po krátkém zamyšlení co vlastně dělá funce call a jaké jsou její parametry, je vše jasné. Metoda fn objektu object1 je volána ve scope object2. Výsledek je tedy následující:

object1: 2

a to díky konstrukci this.variable uvnitř funkce fn. Obdobně by bylo možné funkci zavolat pomocí metody apply s jediným rozdílem a to jsou další parametry. Oproti metodě call má metoda apply pole parametrů, naproti tomu call odděluje jednotlivé parametry čárkou.
Po tomto obecném úvodu co vlastně ten scope je, se dostáváme ke konstrukcím ExtJS:

  • Ext.each
  • Events (události)
  • Handlers
  • Ext.MessageBox
  • ...
  • Ext.each( iterable, fn, [scope], [reverse] ) : Boolean

Jak si můžete přečíst v dokumentaci, metodu each object Ext použijete pro průchod polem. Už v tuto chvíli však člověk udělá chybu pokud zavolá:

function fn () {
    console.log('my function');
}
Ext.each(myArray, function (name, index) {
    this.fn();
});

Výsledkem bude pravděpodobně chyba undefined is not a function. Proč? Protože funkce fn neexistuje ve scope funkce volané v rámci iterace. Jednoduchou úpravou přidáním scope parametru, problém odstraníme:

Ext.each(myArray, function (name, index) {
    this.fn();
}, this);

Události/Events/Listeners
Ajax request zde zmíním ještě před událostmi a to proto, abychom si přiblížili jeden z dalších problémů krom scope, který programátoři běžně v ExtJS dělají. U událostí si musíme nejdříve uvědomit, že jsou asynchronní. A nejlépe si vše ukážeme právě na Ajaxu.

var a = 1;
Ext.Ajax.request({
    url: '/script.php',
    params: {
        id: 1
    },
    success: function(response){
        this.a = 2;
    },
    scope: this
});

Pokud bychom v rámci definice funkce success měnili hodnotu proměnné a, jakou bude mít hodnotu po ihned za zavoláním ajax requestu? 1? 2? Záleží na tom, jak rychle bude zpracován samotný dotaz. Proto nedoporučuji takové používání volání a to nejen na úrovni ajaxu, ale i při využití jednotlivých událostí. Ty jsou pokaždé zpracovány jinou rychlostí, tedy vždy si musíte uvědomit, že událost a může být zavolána před události b, pokud mezi nimi neexistuje nějaká posloupnost.
Události jsou jednou z nejpoužívanějších konstrukcí v Ext, bez kterých se neobejdete.

Existují dvě možnosti jak definovat událost:

myComponent.on('myevent', this.myEventHandler);

nebo v rámci definice objektu:

listeners: {
    myevent: this.myEventHandler
}

U obou dvou konstrukcí je možné definovat scope. U první definice přidáním třetího parametru:

myComponent.on('myevent', this.myEventHandler, this);

u druhého zápisu pak existují dvě možnosti. První z nich umožňuje definovat scope v rámci každé události:

listeners: {
    myevent: {
        fn: this.myEventHandler,
        scope: this
    }
}

druhá pak definuje scope pro všechny události:

listeners: {
    myevent: this.myEventHandler,
    scope: this
}

U tlačítek, Message Boxů je možné scope definovat v rámci configu.

Ext.create('Ext.Button', {
    text: 'Click me',
    renderTo: Ext.getBody(),
    handler: function() {
        this.fn();
    },
    scope: this
});

Ext.Msg.prompt('Name', 'Please enter your name:', function(btn, text){
    if (btn == 'ok'){
        // process text value and close...
    }
}, this);

Obecně u všech konstrukcí, u kterých v rámci dokumentace ExtJS narazíte na slovíčko scope či [scope], si dávejte pozor na to, jak voláte funkce definované v rámci jiného contextu.