0

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 ?

  • Your understanding of the problem is wrong. The real issue is how `this` works in javascript. I will answer the direct solution to your problem below but please read the following answer to understand how `this` work: https://stackoverflow.com/questions/13441307/how-does-the-this-keyword-in-javascript-act-within-an-object-literal/13441628#13441628 – slebetman Oct 25 '17 at 09:09
  • 1
    Also, if you are going to be working on modern javascript or C# or swift I suggest you take a second look at the "misdesign". It's being adopted by other languages. It's called a closure which is an abstraction over the concept of local and global variables. – slebetman Oct 25 '17 at 09:31
  • Thanks god, I never going to work on C# or swift, and I now understand why I was naturally distrustful of these "proprietary$$" high-level languages... :D –  Oct 25 '17 at 09:39

1 Answers1

0

Your problem is simply due the the nature of how this works in javascript. The value of this is not bound at compile time nor at runtime but instead very late at call time.

In the following code:

AudioCtx.decodeAudioData(rawData, 
            this._handleDecodeSuccess,
            this._handleDecodeError);

.. the value of this inside _handleDecodeSuccess and _handleDecodeError is not determined at object creation time but instead at the time they are called. And it is the decodeAudioData method that will eventually call them when decoding is complete. This causes the value of this to become something else (depending on how the functions are called).

The modern solution is to statically bind this to the functions:

AudioCtx.decodeAudioData(rawData, 
            this._handleDecodeSuccess.bind(this),
            this._handleDecodeError.bind(this));

Note: the .bind() method creates a new function that wraps your function with this permanently bound to the argument you pass to it.

The traditional solution is to capture this inside a closure like what you have done.

slebetman
  • 109,858
  • 19
  • 140
  • 171
  • Thanks, so, I finally found the "proper way" despite appearances. Javascript have very interesting features, but sometimes it is very annoying how it naturally induces to writes confusing code. I will keep this .bind() method somewhere, maybe a better way for me produce "more readable" code to my tast. –  Oct 25 '17 at 09:33