I have only been programming in JavaScript for about a year. I don’t count tiny simple bits of code for submitting forms when a button is clicked or other simple actions that don’t take any language-specific skill.

I just encountered an issue that I had seen but never recognized before: Anonymous classes. I found a blog post elsewhere that also called this an “anonymous object” which is not really correct. There was a comment that these are object literals.

I have seen this type of anonymous class many times:

var dave = { hair: red, eyes: blue };

This simply creates an object of a unnamed or anonymous class. The variable dave is an object and the initializer is the literal object. This is commonly used to send options to functions by name:

var x = thefunction( 12, { option1: 16, option2: "test" } );

The function called thefunction above takes two arguments with the second being an object. A variable could have been used with an actual named class object and the code would still work. There is no type checking in JavaScript and any missing object member variables, or whatever they are called in JavaScript, are just undefined.

AudioFX

Here is some code based on something I found on the internet by Jake Gordon. I have reformatted it and made a few modifications but his original copyright is included as required. My intent is to show some complex code that includes an interesting use of an anonymous class. There are a few other interesting bits of code here that deserve some analysis. I hope that I describe it properly:

Copyright (c) 2011 Jake Gordon and contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

AudioFX = new function() 
{

    //---------------------------------------------------------------------------

    var VERSION = '0.4.0';

    //---------------------------------------------------------------------------

    thasAudio = false;
    audio = document.createElement('audio');
    audioSupported = function(type) 
    {
        var s = audio.canPlayType(type); 
        return (s === 'probably') || (s === 'maybe'); 
    };

    if (audio && audio.canPlayType)
    {
        hasAudio = {
            ogg: audioSupported('audio/ogg; codecs="vorbis"'),
            mp3: audioSupported('audio/mpeg;'),
            m4a: audioSupported('audio/x-m4a;') || audioSupported('audio/aac;'),
            wav: audioSupported('audio/wav; codecs="1"'),
            loop: (typeof audio.loop === 'boolean') // some browsers (FF) dont support loop yet
        }
    }

    //---------------------------------------------------------------------------

    var create = function(src, options ) 
    {
        var audio = document.createElement('audio');

        if (options.loop && !hasAudio.loop)
        audio.addEventListener('ended', function() { audio.currentTime = 0; audio.play(); }, false);

        audio.volume   = options.volume || 0.1;
        audio.autoplay = options.autoplay;
        audio.loop     = options.loop;
        audio.src      = src;

        return audio;
    }

    //---------------------------------------------------------------------------

    var choose = function(formats) 
    {
        for(var n = 0 ; n < formats.length ; n++)
            if (hasAudio && hasAudio[formats[n]])
                return formats[n];
        return null;
    };

    //---------------------------------------------------------------------------

    var find = function(audios)
    {
        var n, audio;
        for(n = 0 ; n < audios.length ; n++) 
        {
            audio = audios[n];
            if (audio.paused || audio.ended)
                return audio;
        }
    };

    //---------------------------------------------------------------------------

    var afx = function(src, options ) 
    {
        options = options || {};

        var formats = options.formats || [];
        var format  = choose(formats);
        var pool    = [];

        src = src + (format ? '.' + format : '');

        if (hasAudio && format != null ) 
        {
            for(var n = 0 ; n < (options.pool || 1) ; n++)
            pool.push(create( src, options ));
        }

        return 
        {
            version: VERSION,
        
            supported: format != null,

            audio: (pool.length == 1 ? pool[0] : pool),

            play: function() 
            {
                if( format == null || !useSounds )
                return;
                var audio = find(pool);
                if (audio)
                audio.play();
            },

            stop: function() 
            {
                if( format == null || !useSounds )
                    return;
      
                var n, audio;
                for(n = 0 ; n < pool.length ; n++) 
                {
                    audio = pool[n];
                    audio.pause();
                    audio.currentTime = 0;
                }
            }
        };
    };

    return afx;

    //---------------------------------------------------------------------------

}();

var shootSound  = new AudioFX( 'laser',  { formats: ['mp3'], volume: 1.0, loop: false, autoplay: false, pool: 8 } );

When the code runs, the AudioFX variable is created when the new function()… code after it is called. This mechanism to create the AudioFX object is a tiny bit unusual from what I can find on the internet about it. What happens with it later is very interesting.

Again, when processing gets to the AudioFX variable declaration, the code in the given function is executed and a new object is created. Some tests are done in that code to set variables that determine the state of audio processing in the browser, some functions are defined to do some helper work, and then the var afx code is encountered. This afx variable is just a function because the “new” keyword is not there. It is not executed at this time.

The function that is called to initialize AudioFX returns the afx function! There is no afx object in the data sense. It is a function object. AudioFX is essentially an alias for the afx function but the initialization code for AudioFX was called once. This is the clever thing about this scheme because it sets the AudioFX variable to be a function for creating an object but it also runs initialization code when it does that and it shares variables from that initialization code with the function that it then becomes. Those variables hold their data and can be used by the afx function code at any time later.

The last line of code creates a variable shootSound which is a new object as defined by what is returned by the AudioFX function call. Remember that AudioFX became a function object. Since AudioFX is actually the afx function, the arguments are those appropriate for the afx function call. There is a global variable in there so if anyone takes this code, it won’t work exactly as-is.

But what object is shootSound? It is an object of an anonymous class that is defined by the return statement of the afx function! This is where anonymous classes fit into this long monologue.

The original code had some extra handling for when the audio format is not supported but I removed it because it did not work correctly and it tried to access undefined variables.

It is interesting to note that the play() and stop() functions are defined as members of the anonymous class. Nesting like this is not possible in the languages that I normally use so I would have never written code like this. Interestingly, all of this nesting has the effect of encapsulating code within what look like namespaces. The afx name is never seen outside of the AudioFX function so there would be no name conflict with any afx name elsewhere. The only name that is exposed to the global arena is AudioFX which is very nice feature of this code.

This code allows a JavaScript to play multiple overlapping sounds by creating a pool of <audio> elements within the DOM. The programmer must decide how many sounds might need to overlap since the polling requires a pool size or count.

I used this code in Planetary Defense although I may not have enabled it when you read this. If a small speaker icon does not appear in the upper right corner of the play area then audio is not supported in your browser for the mp3 or wav format or I have not updated the files on the server yet. Check back later to see this working. If you must see it working right away, a development version of the game might work at http://pd.rectorsquid.com/xpd.html.