Friday, June 25, 2010

hijacking javascript functions part 1

Javascript is as you may know, a loosely typed language (ie,you can not specify if a variable will be an integer, a float or an array...).
This can be a strength as it offers highly dynamic options that typed languages can not offer but this can also be a real nightmare when you need to debug something that went plainly wrong...
Typed language can offer control at the compilation phase and therefore can ease debugging by tracking bugs at an early stage. Javascript does not offer that.

We will see in this article how we can use the highly dynamic nature of javascript to hijack functions and add control from outside. we will be able to check function parameters at distance and turn the process on/off easily.

Hijack function

What is so nice about javascript is that it is very dynamic and gives you enough power to manipulate functions at a very early stage.
Let's take a little library that deals with strings and add some utility functions: trim, hyphenize, camelize and firstToUpperCase.

The code will be like:

var StringToolBox = {
    camelize: function(str) {
        return str.replace(/-(.)/g, function(m, l){return l.toUpperCase()});
    },
    hyphenize : function(str) {
        return str.replace(/([A-Z])/g, function(m, l){return '-'+l.toLowerCase()});
    },
    firstToUpperCase : function(str) {
         return str.replace(/^([a-z])/, function(m, l){return l.toUpperCase()});
    },
    trim : function(str) {
         return str.replace(/^\s+|\s+$/g, '');
    }
};

For those of you who are new to javascript, this might be hard to understand.
we put all the functions in an object var StringToolBox= {...} in order to avoid conflict with other javascript scripts that could have the same exact function names. Therefore you will call the trim function like so: StringToolBox.trim('   my string     ');
Then we are using Regular Expressions to look for a pattern and change it the way we want.

So far so good!

But what happen if you do no pass the right parameter? If you do not pass anything or for some reasons, in the process an array gets passed??

In order to check that you do get what you want, you will need to add some control on the data itself.
First let's create an helper function (basic):

var DataController = {
    _isString : function(str){
        if(str===undefined) return false;
        if(str.constructor===String)
            return true;
        return false;
    }
};
We will use it this way:

DataController._isString();//should return false
DataController._isString(1);//should return false
DataController._isString("hi!");//should return true

So now let's implement this in our little toolbox:


var StringToolBox = {

    camelize: function(str) {
        if(!DataController._isString(str)) {
             console.log('the first parameter must be a String Object but seen:'+str);
            return false;

        }
        return str.replace(/-(.)/g, function(m, l){return l.toUpperCase()});
    },
    hyphenize : function(str) {
        if(!DataController._isString(str)) {
             console.log('the first parameter must be a String Object but seen:'+str);
            return false;

        }
        return str.replace(/([A-Z])/g, function(m, l){return '-'+l.toLowerCase()});
    },
    firstToUpperCase : function(str) {
        if(!DataController._isString(str)) {
             console.log('the first parameter must be a String Object but seen:'+str);
            return false;

        }
         return str.replace(/^([a-z])/, function(m, l){return l.toUpperCase()});
    },
    trim : function(str) {
        if(!DataController._isString(str)) {
             console.log('the first parameter must be a String Object but seen:'+str);
            return false;

        }
         return str.replace(/^\s+|\s+$/g, '');
    }
};
OK!
As a note, you may decide to handle the error in different ways. Here we just log a message to the console (Firefox will work but this code will fail in IE) but we could throw new Error or do whatever you think is relevant for your application flow.

Now we can be a little bit more confident about our code and where things might get wrong! But as you can see this is rather verbose, we need to copy/paste the same logic in 4 functions, it increases the file size and reduce readability...
If we were in a strongly typed language by defining the type of the first parameter, the interpreter/compiler could handle this for us for free...

But javascript is dynamic enough to allow us to change that!

HIJACK function in use

Before we see how we will build this function, let's see how we use it:

var StringToolBox = {

    camelize: function(str) {
        return str.replace(/-(.)/g, function(m, l){return l.toUpperCase()});
    },
    hyphenize : function(str) {
        return str.replace(/([A-Z])/g, function(m, l){return '-'+l.toLowerCase()});
    },
    firstToUpperCase : function(str) {
         return str.replace(/^([a-z])/, function(m, l){return l.toUpperCase()});
    },
    trim : function(str) {
         return str.replace(/^\s+|\s+$/g, '');
    }
};

DataController.addConstraint(StringToolBox,function(fName,parameters) {

    if(!DataController._isString(parameters[0])){
             console.log('the first parameter must be a String Object but seen:'+parameters[0]);
            return false;
    }
    return true;
});

So as we can see, we moved all the control checking logic outside of the function and
sum it up in one place. Way much less typing (and we do hate typing!), way much less bytes and therefore a smaller file size and better readability!
the addConstraint function takes an object containing the functions as the first parameter and a function that handles the checking. the function itself is a callback that receives the name of the function being hijacked and the parameters it receives.

so now, how do we implement that??

HIJACK function implementation

First of all we need to loop over all the functions in the object, this is going to be easy:

var DataController = {
    addConstraint : function(oPackage, fConstraint) {

        for(var prop in oPackage) {
            DataController.hijack(oPackage,prop,fConstraint);
        }
        return true;
    }
};

the addConstraint just loop through all the elements in the object (oPackage where o stands for object) and call the hijack functions that will get the package, the element being looped over and the callback (fConstraint where f stands for function).
So as you can imagine, the meat is in the hijack function, let's see its implementation:

var DataController = {

    hijack : function(oPackage,fName,fConstraint) {


        //store the original function in a temporary variable, ie, store trim
        var fCode = oPackage[fName];

       //if this is not a function return early, nothing to hijack here
        if(typeof fCode !=='function') return false;


        //redefine the function in the package by overwritting it, ie redefine trim
        oPackage[fName] = function() {

           //every functions have a 'arguments' variable that holds the parameters
           //we just change this into a real array
            var parameters = DataController.toArray(arguments);
          
           //we then use the callback functions that do the controlling
            var ret = fConstraint(fName,parameters);

           //if the callback return false, something went wrong, we do nothing
            if(!ret) return false;

          //the parameter checking was ok so we call our original function, ie trim
            return fCode.apply(oPackage,parameters);
        }
    }
};

Hum, this gets a little bit tricky...
I added comments to help grab the flow.
Basically, we use the very dynamic nature of javascript that allows us to redefine a function and overwrite it.
we first save the function in temporary variable, then we redefine the function by adding the checking callback. if the checking went wrong, we return but if everything was fine, we just call the original function held in the temporary function. This makes use of closure by the way.
the  DataController.toArray(arguments) loops over the arguments and push them into an array. that's all!
the fCode.apply allows us to keep the scope of the package in case the function was using it and allows us to send the parameters back to the original function.
all the functions were using only one argument but there could be several of them. Using apply allows us to solve the problem in one line!

Conclusion

We were able to add some checking onto the parameters sent to javascript functions by hijacking them. It keeps our code clean and reduce the file size.
As we hijack the function, we could easily turn the checking on/off by adding a flag too! Which could come in handy for live vs development environment.

Although the function allows us to check parameters at distance, you still need to know the function and their parameters. Here it was one parameter, all of them being a String. This will not be true for every functions!
If the function changes, you will need to change the checking too.
Could not it be nice if we could check the parameters automagically without even having to know about the funcions themselves??
we will see how we can do this in the next part of this article!

FULL CODE

var DataController = {

    hijack : function(oPackage,fName,fConstraint) {

        var fCode = oPackage[fName];
        if(typeof fCode !=='function') return false;

        oPackage[fName] = function() {
            var parameters = DataController.toArray(arguments);
            var ret = fConstraint(fName,parameters);
            if(!ret) return false;
            return fCode.apply(oPackage,parameters);
        }
    },

    addConstraint : function(oPackage, fConstraint) {
        for(var prop in oPackage) {
            DataController.hijack(oPackage,prop,fConstraint);
        }
        return true;
    },
    toArray : function (arg){

      var ret =[];
      for(var i=0,ln=arg.length;i < ln;i++){
          ret.push(arg);
      }
      return ret;
   },
   _isString : function(str){
        if(str===undefined) return false;
        if(str.constructor===String)
            return true;
        return false;
    }
};

var StringToolBox = {

    camelize: function(str) {
        return str.replace(/-(.)/g, function(m, l){return l.toUpperCase()});
    },
    hyphenize : function(str) {
        return str.replace(/([A-Z])/g, function(m, l){return '-'+l.toLowerCase()});
    },
    firstToUpperCase : function(str) {
         return str.replace(/^([a-z])/, function(m, l){return l.toUpperCase()});
    },
    trim : function(str) {
         return str.replace(/^\s+|\s+$/g, '');
    }
};

DataController.addConstraint(StringToolBox,function(fName,parameters) {

    if(!DataController._isString(parameters[0])){
             console.log('the first parameter must be a String Object but seen:'+parameters[0]);
            return false;
    }
    return true;
});


1 comment:

Maher Salam said...

Nice tutorial, I like the oop approach you took to write this. I will read it more than once until I understand it :).