brainbaking/content/wiki/code/javascript/inheritance.md

14 KiB

+++ title = "inheritance" draft = false tags = [ "code", "javascript", "inheritance" ] date = "2013-03-12" +++

Javascript Inheritance

JavaScript uses prototypal inheritance. This means that Javascript does not distinguish between classes/prototypes and instances and, therefore, we can add our desired behavior directly to the instance.

"new" operator

Zie http://unitstep.net/blog/2008/01/24/javascript-and-inheritance/

Gegeven de volgende functie:

function X(bla) {
  this.x = bla;
  console.log("doeiets");
  this.ding = function() { return this.x; };
}

Wat gebeurt er bij de klassieke manier van een "klasse" initialiseren? Zo:

new X("argument").ding()

Omdat geen klassen bestaan, wordt er eigenlijk een "leeg" object aangemaakt en het prototype van het bestaand object aangepast door call te gebruiken:

var johnDoe = function(){};
X.call(johnDoe, "dinges")
johnDoe.ding()
johnDoe.x ###### "dinges"

Wat gebeurt hier?

  1. function(){} is een closure, dus een function, zonder inhoud.
  2. de call functie roept een functie aan en vervangt de this referentie (context) door het meegegeven argument
  3. Bijgevolg wordt in X(), this.x = bla gedaan, dat wil zeggen dat vanaf nu onze anonieme closure de property x bevat, samen met alle functies die binnen X gedefinieerd zijn.

Merk op dat "bevat" impliceert dat het object johnDoe natuurlijk nu ook geheugen toegekend krijgt om de variabele "x" op te slaan. Dit in contrast met prototypal inheritance, zie volgend stuk.

Eender welke functie heeft een prototype. Een "lege" functie bevat een dynamische "constructor" (de functie zelf) met lege body:

(function(){}).prototype

######= Gewenst gedrag - wat waar plaatsen ######=

  • Indien ik een functie of een variabele heb die anders kan zijn naargelang de implementatie (definiëer de "naamgeving"), plaats deze dan in de constructor functie.
  • Indien ik een functie of een variabele heb die specifiek voor die functie is en niet gaat veranderen, plaats deze dan in het concreet object via this..
  • Indien ik een functie of een variabele heb die ik tijdelijk wens te gebruiken, plaats deze dan in het concreet object via var ((Maak slim gebruik van scope chaining om zaken te encapsuleren!)).

Typisch bevatten constructor functies ook geen return waarden ((dit retourneert dus impliciet undefined)) - we gebruiken deze functies toch altijd in combinatie met de new operator, dus kennen de nieuwe instantie van het object direct toe aan een variabele.

############= prototype gebruiken als inheritance ############=

-> Meer informatie inclusief grafen met uitgebreide uitleg hoe prototype en constructors werken: zie http://joost.zeekat.nl/constructors-considered-mildly-confusing.html

Aangezien we aan objecten hun functions kunnen via .prototype, is het niet moeilijk om een object zich te laten gedragen als zijn "ouder". Neem bijvoorbeeld een dier object, en een concrete ezel implementatie die een extra functie "balk" definiëert.

function Vierpotige() {
   this.aantalPoten = 4;
   this.eetIets = function() {
     console.log("omnomnom");
   }
}

function Ezel() {
  this.balk = function() {
    console.log("IEE-AA enzo");
  }  
}

Ezel.prototype = new Vierpotige();
var ezeltje = new Ezel();
ezeltje.eetIets(); // aha! outputs 'omnom' en 'als ezel fret ik ook nog gras enzo'

Wat gebeurt hier precies?

  1. Vierpotige bevat een property aantalPoten en een functie eetIets.
  2. de constructor functie Vierpotige wordt aangeroepen zodra een Ezel aangemaakt wordt, zodat properties en functies overgenomen worden.
  3. een nieuwe ezel eet iets via een prototype functie van een lege Vierpotige.

Opgelet Vierpotige.prototype bevat enkel de constructor functie zelf en NIET "eetIets", daarom dat we Ezel.prototype gelijk stellen aan een nieuwe lege vierpotige. Dit zou niet werken:

Ezel.prototype = Vierpotige.prototype;
new Ezel().eetIets() // kapot

Tenzij we eetIets definiëren via Vierpotige.prototype.eetIets = function() { ... } - maar dan kan aantalPoten niet meer vanaf een ezel accessed worden.

Nu de prototype property van Ezel en Vierpotige gelijk zijn, kunnen we het prototype uitbreiden met een functie en die direct op ons nieuw ezeltje toepassen:

Vierpotige.prototype.verteer = function() {
  console.log("ist wc bezet??")
}
ezeltje.verteer()

Waarschuwing het prototype van Vierpotige aanpassen past ook elke Ezel instantie aan, dit is énorm gevaarlijk als er publieke properties zoals aantalPoten gedefiniëerd zijn! Want in tegenstelling tot wat velen denken, worden properties niet gekopiëerd! Dus dit zou het aantal poten van ALLE ezels aanpassen:

dier = new Vierpotige();
Ezel.prototype = dier;
new Ezel().aantalPoten ###### 4; // true
dier.aantalPoten = 2;
new Ezel().aantalPoten ###### 4; // false

######= Properties overriden ######=

Prototypal inheritance werkt omdat JS bij elke property lookup kijkt in welk object die referentie gedefiniëerd is. Is dat het huidig object, neem dan die waarde. Indien neen, kijk in het prototype object. Indien neen, kijk in het prototype object van dat object, en zo maar door tot op Object niveau. We kunnen zo ook een property van een prototype zelf overriden, door ander gedrag te definiëren, of zelfs de super aan te roepen:

Vierpotige.prototype.eetIets = function() {
  console.log("vierpotige eten");
}
Ezel.prototype.eetIets = function() {
    Vierpotige.prototype.eetIets(); // "super"
    console.log("als ezel fret ik ook nog gras enzo");
}

######= Built-in JS types extenden ######=

Extend nooit Object.prototype! Waarom? Omdat Eender welk object een instantie van Object is, dus zijn prototype heeft, en met een for(x in prop) deze property nu ineens toegankelijk is voor elk object. Een leeg object { } wordt verwacht géén properties te hebben!

Object.prototype.hack = function() {}
for(x in {}) {
  console.log(x); // print "hack", zou hier niet in mogen komen
}

############ Checken op inheritance ############

Met Javascript kan men door middel van typeof controleren van welk type een variabele is. Dat komt neer op:

  • object ({})
  • function (function() {})
  • string ("")
  • number (-47.2)

Het is niet zo interessant om te gebruiken voor eigen inheritance. Daarvoor dient instanceof:

ezeltje instanceof Vierpotige ###### true
ezeltje instanceof Ezel ###### true
new Vierpotige() instanceof Ezel ###### false

######= Zelf inheritance afchecken met prototype ######=

met constructors

Dit is een zeer beperkte manier dat geen rekening houdt met "inheritance":

function instanceOf(One, Two) {
  return One.constructor ###### Two;
}

instanceOf(Ezel, Vierpotige) // true

Aangezien elk object een .constructor property heeft die afgeleid werd vanuit de constructor functie die aangeroepen werd, kan men op deze manier een simpele check afwegen. Een praktischer voorbeeld is (typeof (new Date()) ###### object) && (new Date().constructor ###### Date).

met proto

Het instanceof keyword kijkt natuurlijk naar de prototype properties van beide objecten om te controleren of object a van object b komt. Dit kan men ook zelf doen:

function instanceOf(One, Two) {
  return One.prototype.__proto__ ###### Two.prototype;
}

instanceOf(Ezel, Vierpotige) // true

Hoe kan dit kloppen?

  1. Herinner u dit statement: Ezel.prototype = new Vierpotige();. Dit stelt de prototype van Ezel gelijk aan die van een vierpotige. Het enige wat in de prototype van Vierpotige steekt is de verteer() functie, de rest werd via de constructor functie overgekopiëerd!
  2. De magic property _ _proto_ _ wordt intern gezet zodra een prototype wordt toegekend aan een object. Aangezien Ezel zelf ook prototype functies heeft specifiek voor ezels, kunnen we die van Vierpotige niet overriden, maar wel gebruiken.

Bekijk iets toevoegen via .property als iets toevoegen aan het algemeen prototype object, en iets rechtstreeks toevoegen via een key als iets toevoegen op een instance van dat prototype object. In andere dynamische talen stelt property de metaClass voor, maar JS werkt enkel met functies.

De betere oplossing: isPrototypeOf()! Zie [magic_properties]({{< relref "magic_properties.md" >}})

_ _proto_ _ is een instance property, .prototype een constructor function property

met properties

Door hasOwnProperty() te gebruiken kan je nagaan of een property overgenomen is of niet. Vanaf JS 1.5.

############= call als inheritance ############=

De klassieke inheritance structuur zoals in Java en C++ kan beter benaderd worden door call te gebruiken. Herbekijk onze ezels:

function Vierpotige() {
   this.aantalPoten = 4;
   this.eetIets = function() {
     console.log("omnomnom");
   }
}

function Ezel() {
  Vierpotige.call(this);
  this.balk = function() {
    console.log("IEE-AA enzo");
  }
}

var ezeltje = new Ezel();
ezeltje.eetIets(); // aha! outputs omnom

Door als eerste statement in de constructor functie van Ezel een call te plaatsen naar onze "parent", kopiëren we alle keys en values die daarin gedefiniëerd staan. In tegenstelling tot prototypal inheritance kost dit dus veel meer geheugengebruik, en is dit beperkter om uit te breiden. We linken eigenlijk impliciet twee functies aan elkaar door waarden over te nemen, maar iets aanpassen aan Vierpotige gaat de Ezel op geen ekele manier doen veranderen.

############= prototypal inheritance toepassen ############=

In plaats van new overal te gebruiken zonder te weten wat hierachter ligt, kan men create ook gebruiken:

if (typeof Object.create !###### 'function') {
    Object.create = function (o) {
        function F() {}
        F.prototype = o;
        return new F();
    };
}
newObject = Object.create(oldObject);

Zie http://javascript.crockford.com/prototypal.html

############ Een minder verbose manier om extra properties te definiëren ############

Zie http://howtonode.org/prototypical-inheritance -

Object.defineProperty(Object.prototype, "spawn", {value: function (props) {
  var defs = {}, key;
  for (key in props) {
    if (props.hasOwnProperty(key)) {
      defs[key] = {value: props[key], enumerable: true};
    }
  }
  return Object.create(this, defs);
}});

Op die manier kan je BaseObj.spawn({'extraProp': 'extraValue'}); gebruiken, zonder de relatieve verbose manier van extra properties te moeten gebuiken die Object.create handhaaft.

############= Prototype JS en Class.create ############=

Javascript frameworks proberen altijd inheritance makkelijker te maken voor klassieke OO developers door functies te modelleren als klassen. In Prototype JS kan men zoiets doen:


var Animal = Class.create({
  initialize: function() {
    this.something = "wow";
  },

  speak: function() {
    console.log(this.something);
  }
});

var Snake = Class.create(Animal, {
  hiss: function() {
    this.speak();
  }
});

Snake leidt af van Animal, en de initialize() functies stellen de constructor functies voor. Wat gebeurt er dan achter de schermen bij Class.create({...})?

  1. Net zoals hierboven wordt de constructor functie via apply aangeroepen (zelfde als call). Enkel wordt als this een lege functie toegevoegd.
  2. Object.extend() wordt gebruikt om alle keys van de parent te kopiëren naar de nieuwe lege functie. (zie [code/javascript/frameworks]({{< relref "wiki/code/javascript/frameworks.md" >}}))
  3. De prototype property van de parent wordt net zoals hierboven gezet op de nieuwe "klasse".
  4. Speciale gevallen zoals een "lege constructor" functie indien nodig, intern bijhouden wat sub- en superklassen van elkaar zijn, etc.

In essentie komt het neer op "syntax sugaring" zodat het klassieke OO model gebruikt kan worden - terwijl er onderliggend iets anders gebeurt.

Meer info over deze implementatie: http://code.google.com/p/inheritance/

############= Multiple inheritance ############=

Perfect mogelijk, of slechts delen van object A en alles van B voor object C gebruiken (mixins!). Simpelweg alles van de ene prototype property naar de andere overzetten wat nodig is:

######= Methode 1 ######=

function A(){};
A.prototype.a = function() { return "a"; }
A.prototype.c = function() { return "a"; }
function B(){};
B.prototype.b = function() { return "b"; }

function C(){};
for(prop in B.prototype) {
  C.prototype[prop] = B.prototype[prop];
}
C.prototype.a = A.prototype.a;
C.prototype.c = function() { return "c"; }

var c = new C();
c.a() ###### "a";
c.b() ###### "b";
c.c() ###### "c";

######= Methode 2 ######=

Creeër de illusie om constructor(s) aan te roepen in een constructor functie van een ander object:

function B() {
  this.b = function() { return "b"; }
}
function C() {
  this.c = function() { return "c"; }
}
 
function A() {
  this.a = function() { return "a"; }
  this.super = B;
  this.super2 = C;
  this.super(); // kopiëer de b functie in A, maar inherit niet!
  this.super2();
}


var a = new A();
a.a() ###### "a";
a.b() ###### "b";
a.c() ###### "c";

######= Problemen met mixins ######=

Dit is géén authentieke multiple inheritance. Hier zijn twee problemen aan gekoppeld:

  1. Zodra via B.prototype een nieuwe functie toegevoegd wordt, zal C deze niet overpakken omdat C.prototype niet gelijk gesteld werd aan die van A of B
  2. En bijgevolg dus ook de instanceof operator naar de zak is:
c instanceof C ###### true
(c instanceof B || c instanceof A) ###### false

Als dit echt nodig is kan men zoiets zelf implementeren door weer te loopen via for(prop in x.prototype) { ... } en te checken of alle keys voorkomen in een object.

Zie Mozilla Dev center: Details of the Object Model