Showing posts with label closure. Show all posts
Showing posts with label closure. Show all posts

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;
});


Saturday, September 29, 2007

My inner request for Outer !

If you read the preceding entry basics of easing and that your are not familiar with javascript, you were perhaps surprised with the following code concept:
function greetings(msg) {
        function display() {
            alert(msg);
        }
        display();
}
So what's going on there ?
We have a main function that receives a string as a parameter.
Nothing extraordinary here!
Then we define a function within the main function that will be in charge of alerting the parameter.
Here it's a little bit unusual!
In fact, you can define a function from within a function in javascript and you can nest the functions as will !
The inner function can access the global variables define within the scope of the outer function.
Why is it so cool will you ask?
Well, the cool thing about inner functions is that they will work even if the outer function as returned and this can be very useful for dealing with variable scoping.
You can write the above inner function with an anonymous function if you want:
function greetings(msg) {
        (function() {
            alert(msg);
        })();
}
This is an anonymous function as it does not have any name !
Enclosing an anonymous function with parentheses ()();, will cause the function to be executed right away !

Ok, this example is very simple and is not very useful, so let's see something more powerful.
If you do the following, what's going to happen ?

function myEvents(elm,events) {
   for(i=0;i < events.length;i++) {
      elm['on'+events[i]]=function () { 
           alert(i);
        }
   }
}
myEvents(document.getElementById('the_click'),['mouseover','mouseout']);
try me
if you try this function, mouseovering and mouseoutting (it's new) the html element will alert... 2 every time !
We could have expect 0 for mouseover and then 1 for mouseout but no,no.
Why then ?
This is a scoping problem;
myEvents function, the main function has elm, events and i as variables.
As I said, inner functions can access the variables of the outer functions but one important thing:
If you don't call the inner function during the loop,the function will accede the value of the variables once the outer function has returned !
That is why this outputs the last value of i and not the value in between.

So how can we output the right value ?
As this is a scoping problem, we need to scope the value of the variable to the scope of an inner function that we will call right away.
function myEvents(elm,events) {
   for(i=0;i < events.length;i++) {
      function display() {
      elm['on'+events[i]]=function () {
           var n=i; 
           alert(n);
        }
      }
      display();
   }
}
myEvents(document.getElementById('the_click'),['mouseover','mouseout']);
try me
Tadam !

It doesn't work !
If you have a closer look to the function above, we are trying to scope the value of i within the anonymous function applied to the object.
But this function is not executed right away, it will be executed when you will mouseover or mouseout on the link.Therefore, this function is calling the last value of the outer function.
We need to capture the value within an inner function that will be executed right away.
So...
function myEvents(elm,events) {
   for(i=0;i < events.length;i++) {
      function display() {
        var n=i; 
      elm['on'+events[i]]=function () {
           alert(n);
        }
      }
      display();
   }
}
myEvents(document.getElementById('the_click'),['mouseover','mouseout']);
try me
Tadam !

It works !
In this last function, we capture literally the value of the global i to the scope of the display function by putting it in the variable n.
While we have this value under the hat,in our scope, we execute the function that will refer to the value we want !
If writing and finding a name to this 'capturing' function is a pain, just short it up:
function myEvents2(elm,events) {
   for(i=0;i < events.length;i++) {
      (function () {
           var n=i;
      elm['on'+events[i]]=function () { 
           alert(n);
        }
      })();
   }
}