javascript: class methode als callback handler

Zo af en toe leer je iets over een ontwikkel-taal of -omgeving waarvan je je afvraagt waarom je dat niet een paar jaar eerder wist. Dat overkwam mij toen ik bezig was met een stuk javascript waarbij ik een class had waarvan ik een methode als callback handler wilde gebruiken.

Om het probleem (en de oplossing) te illustreren heb ik een simpel voorbeeld gemaakt van zo’n situatie. Een class die de totale oppervlakte (in pixels) kan berekenen van plaatjes.

function ImageAreaCalculator(counterElement) {
    this.totalArea = 0;
    this.counterElement = counterElement;

    this.addImage = function(src) {
        var img = document.createElement('img');
        img.src = src;
        img.onload = this.onloadImage;
        document.appendChild(img);
    };

    this.addDimensions(width, height) {
        this.totalArea += (width * height);
    };

    this.updateCounter = function(counterElement) {
        counterElement.innerHTML = this.totalArea;
    }; 

    this.onloadImage = function(e) {
        var img = e.target;
        this.addDimensions(img.width, img.height);
        this.updateCounter(this.counterElement);          
    };

}

var counterElement = document.createElement('div');
document.appendChild(counterElement);
var calc = new ImageAreaCalculator(counterElement);
calc.addImage('foo.gif');

Bovenstaande code werkt helaas niet. In de methode onloadImage() zit een aanroep naar ‘this’ en op het moment dat die regel wordt uitgevoerd is ‘this’ niet meer het object ‘calc’ maar het ingeladen plaatje ‘img’.

Een workaround is dat je het object waar je de callback iets mee wilt laten doen injecteert in het img-object. De callback handler haalt dat object vervolgens weer uit het target dat wordt meegegeven in het event-object:

this.addImage = function(src) {
    var img = document.createElement('img');
    img.calc = this; 
    img.src = src;
    img.onload = this.onloadImage;
    document.appendChild(img);
};
this.onloadImage = function(e) {
    var img = e.target;
    var calc = img.calc;
    calc.addDimensions(img.width, img.height);
    calc.updateCounter(calc.counterElement); 
};

Niet echt fraai. Deze opzet vereist namelijk een ongeschreven afspraak tussen de aanroeper en callback dat er een magisch ‘calc’ object wordt gezet in het event-target.

Er is echter een manier om de onload-callback te maken zoals in dit geval gewenst is; dat ‘this’ ook echt verwijst naar de juiste instantie. Daarvoor is de bind() methode die bestaat voor elke functie (ja; ik was ook verbaasd dat een functie automatisch van bepaalde functies wordt voorzien).

Door het aanroepen van bind() op een verwijzing naar een functie creëer je een nieuwe callback die de waarde voor ‘this’ instelt op de meegegeven variabele. Hier een voorbeeld van zo’n callback:

this.addImage = function(src) {
    var img = document.createElement('img');
    var callback = this.onloadImage.bind(this);
    img.src = src;
    img.onload = callback;
    document.appendChild(img);
};
this.onloadImage = function(e) {
    var img = e.target;
    this.addDimensions(img.width, img.height);
    this.updateCounter(this.counterElement);          
};

Op het moment dat onloadImage() wordt aangeroepen heeft ‘this’ de waarde die je zou verwachten en werkt de aanroep naar addDimensions() en updateCounter() dus ook zoals je zou willen.