I was facing a problem with callbacks in Javascript. I solved my problem using what I would call an ugly property of Javascript (so to say, something that would logically be forbiden and never work in other languages than Javascript). So my question: Is there an ELEGANT way, to do the same thing.
I will so begin with the beginning. My goal was to wrap, in some manner, the Web Audio API. In the architecture, I implemented a class, lets call it AudioRessource
, which is destined to be an interface (abstraction) in some manner of the AudioBuffer
object of the Web Audio API.
This class (AudioRessource
) have a prototype member function that must simply take an url as argument to automatically load audio data, decode it, handle errors, etc and finally hold the resulting AudioBuffer
object in a "pseudo-private" member:
function AudioRessource()
{
this._aBuffer = null; // future reference to `AudioBuffer` object
this._loadStatus = 2;
};
AudioRessource.prototype.loadData = function(url) {
/* deal here with async functions to
provides audio data loading automation */
}
The main problem here, is that this will be an object instance (of AudioRessource
) which will create the callback functions, using only local references, and must be able to pass the final AudioBuffer
object to itself.
To load the raw audio data, this is pretty simple, I use the XMLHttpRequest object, with an extra property set as member of the XMLHttpRequest object, like this:
AudioRessource.prototype.loadData = function(url) {
let req = new XMLHttpRequest();
req.extraProperty = this; // reference to `AudioRessource` instance
req.onload = function(){
// retrive instance reference within the callback
this.extraProperty._loadStatus = 0;
}
req.onerror = function(){
// retrive instance reference within the callback
this.extraProperty._loadStatus = -1;
}
req.open('GET', url, true);
req.send(null);
this._loadStatus = 1;
}
The big problem appear when we have to decode the coded raw audio data into PCM data, that is, an Web Audio API AudioBuffer
object instance. Indeed, the Web Audio API provides only one function to achieve this, and this function is asynchronous, and takes a callback that simply recieve the resulting buffer as argument: how to "catch" this resulting buffer to assign it to the proper AudioRessource
instance (the one who lauched the process) ? This work that way:
AudioCtx.decodeAudioData(rawData,
function(result){
// do something with result },
function(error){
// do something with error });
My first naive approach, was to think like we were in C/C++ : I simply put an AudioRessource
instance function "pointer" (reference) as callback, this way, the AudioRessource
instance will directly recieve the buffer:
// where 'this' is an `AudioRessource` instance
AudioCtx.decodeAudioData(rawData,
this._handleDecodeSuccess,
this._handleDecodeError);
However, this does not work, because in javascript, this is not a "function pointer" that is passed into the decodeAudioData
, but if I well undstand, an literal expression, that is, the "ASCII content" of the function... So the 'this' reference is lost !
I spent some time to try understand how this kind of asynchronous function is attended to work, since to me, coming from C/C++, this is simply an heresy: The function does not take any extra argument, no way to pass any external reference... "What is that thing ?". Then I finaly decided to try the "Illogical Javascript logic" way... And I found the solution :
// Create local variable which stores reference to 'this'
let thisInstReference = this;
// Use the local variable to write our callback
AudioCtx.decodeAudioData(rawData,
function(resut){
thisInstReference._aBuffer = result;
thisInstReference._loadStatus = 0;
},
function(resut){
thisInstReference._loadStatus = -3;
});
To be honnest, to me, this is simply freaking. First of all, I even don't understand what realy happen: HOW a local variable (to a object instance's member function), that stores a reference to an object instance (this), can be used "as this" in a callback function ? I do not even understand how a language can allow this kind of thing. Secondly, to me, this not a "proper way" to code something: this code is simply illogical, dirty, this works but this appear as an ugly hack that takes advantage of Javascript misdesign.
So here is my question: How to achieve this, in a elegant way ?