Objecten in javascript - Overerving

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


Overerving

Dit onderdeel beschrijft hoe verschillende objecten eigenschappen functies van elkaar kunnen overerven. Dit vereenvoudigt en verkort de benodigde code voor de meeste scripts, en biedt zo overzicht en onderhoudbaarheid.

Vooraf

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

Overerving is leuk, maar zou voor script "applicaties" al snel overkill kunnen zijn. bedenk je goed wanneer en waarom je kiest het wel of niet te gebruiken.

Overzicht

De tutorial bestaat uit de volgende onderdelen:

Overerving

Objecten met eigenschappen van andere objecten

Bij overerving definiëer je niet alleen verschillende objecten, maar koppel je ze ook nog zo aan elkaar dat het ene object eigenschappen en functies "erft" van het andere object, en deze dus zelf ook overal gebruiken kan.

Stel je maakt een game in javascript, waarin een speler tegen verschillende types monsters moet spelen. Het is onvermijdelijk dat deze monsters enige overeenkomsten hebben; een hoeveelheid "hits", misschien munitie voor een wapen, maar ook bijvoorbeeld een remove() functie die het monster weghaalt als de speler hem verslaat. Het zou onnodig en dubbel werk zijn om deze eigenschappen en functies voor elk type monster apart te coden ...

Bekijk het onderstaande voorbeeld.

function Monster(name) {	
   // "Super" object
   this.name = name;
}
   Monster.prototype.show = function() {
      alert(this.name);
   }

function Ghost(name) {
   this.name = name;

   // dit werkt nu niet
   this.show();
}

function Wolf(name) {
   this.name = name;

   // dit werkt nu niet
   this.show();
}

Wat je eigenlijk zou willen is dat de show van Monster ook beschikbaar is in Ghost en Wolf, zodat deze niet apart hoeven worden toegewezen, en dat zou uiteindelijk in minder code en meer duidelijkheid resulteren. De manier om dit te doen is om de prototype van het object te overschrijven met een instantie van het object waar het van erven moet.

Hiermee voorzie je het prototype in 1 keer van alle functies die het Super object ook had. Dat klinkt waarschijnlijk vreemder dan het eruit ziet:

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

   // etc

function Ghost(name) {
   this.name = name;
   this.show();
}
   Ghost.prototype = new Monster();

function Wolf(name) {
   this.name = name;
   this.show();
}
   Wolf.prototype = new Monster();


new Ghost('Caspar');
new Wolf('Lassie');

Omdat de prototypes nu overschreven zijn met een nieuw Monster krijgen alle toekomstige instanties van deze objecten ook de functies van Monster. Omdat je bij het overschrijven van de prototype geen "bruikbare" instantie nodig hebt is het niet nodig om parameters mee te sturen aan de Super.

Let er wel op dat alles wat in de constructor van de Super staat (probeer bijvoorbeeld een alert) gewoon uitgevoerd wordt bij het toewijzen aan een prototype van een ander object. Op zich is dat niet erg, maar als die constructor html zou genereren is het verstandig om een check in te bouwen dat dat niet gebeurt als er een instantie wordt gevraagd voor overerving; Dat kan heel makkelijk door gewoon de functie te stoppen als hij geen parameters krijgt:

function Monster(name) {
   if(!name) return; // overerving check
   
   // ...
}

Een stap verder

Naast het laten overerven van functies zou je eigenlijk ook willen dat je de parameters van de monstertypes in het super object kan laten afhandelen om de in de Monster constructor toegewezen eigenschappen ook in Ghost en Wolf te kunnen laten toewijzen. Nu staat er in zowel Ghost als Wolf:

this.name = name;

Nu is dat met 1 property geen probleem, maar dat wordt het wel als je een instantie van een monster creeert aan de hand van bijvoorbeeld meerdere parameters:

function Ghost(x, y, width, height, name, life) {
   this.x = x;
   this.y = y;
   this.width = width;
   this.height = height;
   this.name = name;
   this.life = life;
}

function Wolf(x, y, width, height, name, life) {
   // idem
}

// etc.

Zowel in de constructor van Ghost als die van Wolf en ook alle andere eventuele monster types zou elke keer datzelfde rijtje properties staan. Dat is dubbelop en dus overbodig, en bij het maken van een aanpassing in je code zou je dit op meerdere plaatsen moeten doen. Dat vraagt om problemen, dus moet dit ook anders kunnen.

Waar de prototypes van Ghost en Wolf overschreven worden door een instantie van Monster kan je deze properties niet meesturen. Deze aanroepen worden immers maar 1 keer gedaan om straks te kunnen gelden voor alle toekomstige instanties. Op die toekomstige instanties is daarvandaan dus geen verdere invloed uit te oefenen.

Om de constructor van Monster aan te kunnen roepen moet er dus wel iets in de constructor van Ghost en Wolf zelf opgenomen worden, een andere plek is er niet. Alleen zo kunnen de properties die binnenkomen doorgestuurd worden naar de Super die ze vervolgens toewijst. Daar komt nog bij dat this. uit Monster in het geval van overerving op Ghost of Wolf moet slaan, en niet meer op Monster zelf. Dit kan allemaal met de apply() functie, die standaard in javascript beschikbaar is:

function Monster(x, y, width, height, name, life) {
   this.x = x;
   this.y = y;
   this.width = width;
   this.height = height;
   this.name = name;
   this.life = life;
}
   Monster.prototype.show = function() {
      alert(this.name+' '+this.life); // enz.
   }

function Ghost(x, y, width, height, name, life) {   
   Monster.apply(this, arguments);
   this.show();
}
   Ghost.prototype = new Monster();

function Wolf(x, y, width, height, name, life) {
   Monster.apply(this, arguments);
   this.show();
}
   Wolf.prototype = new Monster();


new Ghost(30,30, 25,25, 'Caspar', 100);
new Wolf(100,20, 25,10, 'Lassie', 130);

Door de apply worden de parameters die Ghost en Wolf binnenkrijgen doorgestuurd naar Monster, en worden deze daarbinnen toegewezen aan de Ghost of Wolf instantie. Dit gebeurt door de 2 parameters die aan de apply worden meegegeven; this als verwijzing naar het huidige object, en arguments, een lokale array die standaard binnen elke functie beschikbaar is en de doorgestuurde parameters bevat.

Eigenlijk is het nu dan ook niet nodig om x, y, enz in Ghost en Wolf als parameters te definieren, aangezien de apply toch alle binnenkomende parameters doorstuurt, echter is het wel handig als je nog meer parameters aan het object wil meegeven die specifiek zijn voor het nieuwere object. Ook is het beter voor je eigen overzicht, en om fouten (bv in de parametervolgorde) te voorkomen.

Apply is verder onafhankelijk van de prototype constructie die het overerven van functies mogelijk maakt, en kan dus ook met andere functies en objecten worden aangeroepen, en dus ook voor totaal andere doeleinden worden gebruikt.

Samenvattend

Overerving in javascript zit een beetje raar in elkaar. Het is misschien te beschouwen als een soort hack, aangezien je er er 2 constructies voor nodig hebt die misschien niet direct intuitief of elegant genoemd kunnen worden. Het doet echter niets af aan het feit dat je zo wel volwaardige overerving kan gebruiken. Het is een practische techniek om code binnen een javascript applicatie flexibel, herbruikbaar en uitbreidbaar te houden.

Alles onder de knie? ga dan verder naar het laatse onderdeel

Peter Nederlof - peterned