Objecten in javascript - Scope

Een tutorial over het gebruik van objecten in javascript, door Peter Nederlof. 2 maart 2004.


Scope

Dit onderdeel beschrijft hoe er gebruik te maken is en invloed uit te oefenen is op de scope van javascript. Dit is misschien meer een optioneel onderdeel, maar daarom niet minder handig om onder de knie te krijgen.

Vooraf

Dit onderdeel van de tutorial gaat er vanuit dat je al aardig overweg kan met OO javascript. Dat wil zeggen; Het gebruik en de werking van variabelen en methoden en het daarmee opzetten van een eenvoudige object-structuur gebruik makend van overerving. Is dat niet het geval dan is het aan te raden je hier toch nog iets meer in te verdiepen.

De scope van je script controleren en die van anderen hun script volgen is nog wel te doen, maar pas als je het echt snapt kan je zieke dingen gaan doen. Je hoeft dit niet te snappen om toch fatsoenlijk OO te kunnen scripten, maar het kan ook zeker geen kwaad

Overzicht

De tutorial bestaat uit de volgende onderdelen:

Scope

De scope bepaalt welke variabelen tijdens de uitvoer van een script al dan niet beschikbaar zijn en bij welk object de code op een belaald moment hoort; Het object waar this naar verwijst. Normaal gesproken bepaalt een script zelf de juiste scope, en heb je er zo goed als geen omkijken naar. Er is echter ook invloed op de scope uit te voeren, zo is de scope te "vangen" voor later gebruik, of op te slaan in een variabele.

Om het gebruik van de scope te illustreren gaan we in dit onderdeel de hiervoor beschreven overerving vereenvoudigen tot 1 functie-aanroep. Deze functie gaat daarvoor de prototype van het object overschrijven met een instantie van het Superobject, en bovendien die Super als functie in de scope van het nieuwe object aanroepen. Hiervoor bekijken we eerst nog een keer de Apply.

Apply

De apply truuk die je net zag om overerving mogelijk te maken is op meerdere manieren in te zetten. Apply is op zich niets meer dan een standaard methode van het functie object. Omdat functies ook objecten zijn kan de apply op elke functie uitgevoerd worden. Als je apply op een functie uitvoert wordt deze uitgevoerd alsof deze bij het object hoort dat als eerste parameter meegegeven wordt. Daarbij krijgt de functie de parameters binnen die de apply als 2e parameter krijgt meegestuurd; Apply roept dus een functie aan alsof deze in de scope van het opgegeven object staat.

Om dat wat duidelijker te laten zien:

function User(name) {
   this.name = name;
}

   function showMe() {
      alert(this.name);
   }
  
user = new User('henk');
showMe.apply(user, []);

User is een object die verder geen functies heeft. ShowMe hoort niet bij het User object, en is dan ook niet zomaar aan te roepen om de naam van user (henk) te tonen. Door apply uit te voeren op showMe kan dat wel. De 1e parameter is user, het object waarop showMe uitgevoerd gaat worden, als 2e parameter staat er in dit geval een lege array (die ook weggelaten had kunnen worden), omdat er geen parameters nodig zijn voor showMe.

Waarom is apply zo tof? omdat je de scope van aanroepen op alle mogelijke manieren kan manipuleren. Is die zo bijzonder dan? Dat ook weer niet. Met een beetje moeite kan je hem ook zelf nabouwen, maar zeker wat betreft het komende stuk is die EN handig in gebruik, EN handig om van te leren.

Overerving in de herhaling

In het vorige onderdeel bleek dat je voor overerving 2 constructies nodig hebt; Een om het prototype te veranderen, en een om de Super aan te roepen die de paramters kan afhandelen in de scope van nieuwe object instanties. Dat is ook best te combineren tot 1 functie, zodat overerving er weer wat mooier uitziet.

Omdat deze constructie de werking van een functie gaat veranderen moet hij gekoppeld worden aan de prototype van Function. Bovendien omdat extends een gereserveerd woord is een andere naam voor de functie gebruikt worden. De basis zou er dan ongeveer zo uitzien:

Function.prototype.extendsFrom = function(Super) {
   this.prototype = new Super();
}

Als extendsFrom nu aangeroepen wordt op een functie staat de scope binnen de extendsFrom op de constructor functie van het te veranderen object! De regel met this.prototype is daarin dan de aanroep die het prototype van deze functie overschrijft. Super moet daarvoor als functie referentie meegegeven worden om hem binnen de extendsFrom te kunnen instantieren. Daarmee is het eerste deel van overerving klaar, en zou een Ghost van Monster kunnen overerven met de volgende aanroep:

Ghost.extendsFrom(Monster);

// binnen extendsFrom vertaald:
//  this.prototype = new Super();
// Ghost.prototype = new Monster();

Daarnaast moet alleen ook nog die Super (Monster) worden aangeroepen op het moment dat er een instantie gemaakt gaat worden van de vernieuwde Ghost. Hiervoor moet binnen de extendsFrom de werking van this aangepast worden. Dit is alleen niet mogelijk. Wat wel kan is zowel de Super als this gebruiken in een nieuwe functie, en deze door de extendsFrom terug te laten geven. Het volgende voorbeeld is een eerste aanzet:

Function.prototype.extendsFrom = function(Super) {
   this.prototype = new Super();

   var Func = function() {
      Super.apply(this, arguments);		
   }
   
   return Func;
}

De scope staat dit gebruik van Super binnen Func toe, net zoals een gewone globale variabele ergens verderop in een script binnen een functie te gebruiken is!

Dit werkt zo alleen niet, want nu is het nodig dat je niet de prototype van this overschrijft, maar die van de nieuwe functie. Deze geef je immers terug en de oude functie gooi je eigenlijk weg. Daarom moet nu niet alleen de Super, maar ook de constructor van het nieuwe object zelf aangeroepen worden in de nieuwe scope, omdat Func zelf nog "leeg" is. Binnen Func zal this alleen niet meer in de scope staan van extendsFrom; Daarom moeten we deze scope even in een variabele "vangen" die wel binnen Func te gebruiken is, bijvoorbeeld "Self". Uiteindelijk komt het er zo uit te zien:

Function.prototype.extendsFrom = function(Super) {
   var Self = this; 
   var Func = function() {
      Super.apply(this, arguments);
      Self.apply(this, arguments);
   }
   Func.prototype = new Super();
   return Func;
}

Func wordt dus ook pas uitgevoerd bij het maken van instanties. Toch blijven de eerder aan extendsFrom doorgegeven variabelen en opgeslagen scopes behouden, en zal het inderdaad werken. Hoewel het een algemeen aanvaarde conventie is om variabelen te beginnen met een kleine letter zou ik dat hier niet doen, omdat je met constructors (of classes) bezig bent, en daar is de conventie wel een hoofdletter.

Om de extend te gebruiken:

function User(name) {
   this.name = name;
}
   User.prototype.show = function() {
      alert(this.name);
   }

function Admin(name) {	
}
   Admin = Admin.extendsFrom(User);

admin = new Admin('henk');
admin.show();	

Let wel, dit is maar 1 manier om dit te doen. Er zijn er vast ook meer, zoals alles eigenlijk wel op meerdere manieren te doen is. Als je echt de diepte in wil kan je functies als de apply dus ook zelf bedenken en bouwen. Probeer hem bijvoorbeeld maar eens na te bouwen. Internet Explorer 5.0 ondersteunt hem namelijk niet ;)

Timers

Nu zonder globale array...

Om terug te komen op timeouts en intervals; Zoals getoond in de extendsFrom functie kan een bepaalde objectscope opgeslagen worden in een variabele om deze vervolgens later te gebruiken. Dit is ook toe te passen voor timeouts en intervals. In plaats van een string kan je aan een timeout of interval namelijk ook een functie of functiereferentie meegeven:

setTimeout(function() { alert('hoi'); }, 1000);

Binnen die functie zou dus ook een variabele uit een omvattende scope gebruikt kunnen worden, zoals dit met de Self variabele in extendsFrom gebeurt. Eigenlijk is er dan ook geen verschil:

function Counter(nr) {
   this.time = 0;
      
   var self = this;
   setInterval(function() {
      self.update(1);
   }, 1000);
}

   Counter.prototype.update = function(time) {
      this.time += time;

      // tonen in statusbalk
      window.status = this.time;
   }

new Counter(0);

Tot slot

Als je alles onder de knie hebt, en dus waar jij wil objecten en functies en het verloop van de scope kan beinvloeden en aanpassen is er buiten wat de taal echt niet toestaat eigenlijk niets wat je niet met OO doen kan. Daarnaast werkt het gewoon prettiger en effectiever.

Peter Nederlof - peterned