Dynamic function definition in JavaScript

In JavaScript, you worry about whether your code will run on your user's browser. There are two options that I know of that you can use to make sure your code execute correctly. Browser detection and feature detection.

Overview

There are different reasons why you want to get some informations about the browser running the code. If some features are not supported for example you want to notify the user that maybe it is time to update or make the switch. Each method of checking comes with its own sets of challenges. The goal is to give the user the best experience possible. I will give a quick overview of each before I dig into dynamic function definition.

Browser Detection

With browser detection, you have to parse the User Agent to determine which browser it is and run the code that is known to work. In JavaScript, the user agent string is provided by the navigator.userAgent API. There are too many different user agents available. Each browser comes with it's own sets and from one version to another, they can vary considerably. Obviously it is not the optimal way to write code supported on all browsers. To add to this nightmare, the user can also change the user agent to be something completely custom. In some rare cases you actually want to know if the browser is Internet Explorer and there is a solution for that.

Browser User Agent String
Chrome 32.0.1667.0 Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36
Chrome 32.0.1664.3 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1664.3 Safari/537.36
Internet Explorer 11 Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko
Internet Explorer 10.0 Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident/6.0)
Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)
Safari 6.0 Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25
Safari 5.1.7 Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13+ (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2

List of Browser user agents. — See the complete list

Feature detection

Feature detection is a simpler solution. You check if the browser supports a specific feature. If it doesn't, you use another method to get the same results. For example most modern browsers support the document.getElementsByClassName API to select all elements with a specific class name. But if your user has an older browser you will have to find another way of doing it:

function getElByClassName(className){
    if ("getElementsByClassName" in document){
        return document.getElementsByClassName(className);
    }
    var allElements = document.getElementsByTagName("*"),
        filteredElements = [],
        i, // counter
        l = allElements.length,
        currentClass;

    for(i=0;i<l;i++){
        currentClass = allElements[i].className;
        if (currentClass.indexOf(className) !== -1){ // sure use regex here
            filteredElements.push(allElements[i]);
        }
    }
    return filteredElements;
}

In this function, we check if the document object contains getElementsByClassName method. If it does, we use it and exit the function. Otherwise we use our own home-brewed method. We don't care what browser the user has as long as the method works.

Dynamic function definition.

For the purpose of this article, feature detection is the winner and we will explore it a little deeper. What we want to do is improve this method to make it much faster and efficient.

The problem

Let's use event handling as an example for a cross-browser solution:

function addEvent(element,event,func){
    if (element.attachEvent){
        element.attachEvent("on" + event,func);
    }else if(element.addEventListener){
        element.addEventListener(event,func,false);
    }else {
        // Geez! what browser are you running
    }

}

With this solution, every time we want to add an event, we have to check if the browser supports the feature. It doesn't have a big performance impact but if we can make the script only check it once and use the correct method afterwards, that will be an improvement.

The solution

When the script is first loaded we can run all our test to determine the methods the browser supports and create the right function for the job.

Let's try it with the event handler again:

var Handler = (function (){
    var addEvent,
        removeEvent,
        hand = {},
        on = "",
        attach = "attachEvent",
        detach = "detachEvent";
    if(window.attachEvent){
        on = "on";
    }else {
        attach = "addEventListener";
        detach = "removeEventListener";
    }
    addEvent = function (elem,event,func){
        elem[attach](on+event,func,false); // bubbling will be ignored in IE
    };
    removeEvent = function (elem,event,func){
        elem[detach](on+event,func);
    };
    hand.addEvent = addEvent;
    hand.removeEvent = removeEvent;
    return hand;
})();

In this example, we figure out from the beginning what function is supported. We could create a function for each version and only assign the correct one to the hand object but that would be too repetitive. Here is how this function will be used:

var doSomething = function (){
    // do great things;
    // then remove event
    Handler.removeEvent(this,"click",doSomething);
};
var button = document.getElementById("the-button");
Handler.addEvent(button,"click",doSomething);

As an excercise let's try to convert the function we had earlier getElByClassName to this method:

var Handler = (function (){
    ... 
    var getElByClassName;
    if ("getElementsByClassName" in document){
        getElByClassName = function (className){
            return document.getElementsByClassName(className);
        }
    }else {
        getElByClassName = function (className){
            var allElements = document.getElementsByTagName("*"),
            filteredElements = [],
            i, // counter
            l = allElements.length,
            currentClass;

            for(i=0;i<l;i++){
                currentClass = allElements[i].className;
                if (currentClass.indexOf(className) !== -1){ // sure use regex here
                    filteredElements.push(allElements[i]);
                }
            }
            return filteredElements;
        };
    }
    hand.getElementsByClassName = getElByClassName;
    ...
    return hand;
})();

And of course you can use it like so:

Handler.getElementsByClassName("class-name");

Every elements with this class name will be returned regardless the browser supports it or not.

Conclusion

There is no need to try to detect the features the browser supports every single time you are going to use them. When your script is downloaded on the page, all the testing can be done and your API will know exactly what method to use to perform a task. The performance improvement on the examples I provided may be small, but the bigger your script gets the more complex it becomes and every little optimization that can be done will be a great improvement.

Happy Coding.


Comments

There are no comments added yet.

Let's hear your thoughts

For my eyes only