Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Passing scope to callback function / binding

I am attempting to pass function scope to a callback method. The problem I am having is that I am getting object scope, which does not provide me with access to the parameters and local variables in the original function. My understanding of "this" means the current context (whether it be window or some object) in addition to locally declared variables and parameters. [cite Richard Cornford's excellent work at http://jibbering.com/faq/notes/closures/ on "Execution Contexts" section]. I also understand that variables in JavaScript have function scope (that if they are declared inside a function, they are only accessible from within that function).

With that understanding, in a new environment, I am trying to code a pattern that I did alot for my previous employer, calling to an asynchronous method, specifying a callback handler and passing my current scope, expecting it to be available in the callback method. I am not finding this to be the case in my current environment. (disclosure: i was using ExtJS in my previous environment... making me feel now like I was a bit too cozy w/ the framework, making assumptions about what was going on).

My simple test code demonstrates what I am trying to do and what does not work.

function myHandler(data, ctx) {
    console.log('myHandler():  bar: ' + bar);  // <- prob: bar undefined 
    console.log(JSON.stringify(data));
}
MySvcWrap = {
    doWork: function(p1, callback, scope) {
        var result = {colors: ['red', 'green'], name:'Jones', what: p1};
        if (callback) {
            callback.call(scope||this,result, scope);
        } 
    }
}
function lookup() {
    var bar = 'food'; // local var
    MySvcWrap.doWork('thang', myHandler, this); // scope object is this
}

lookup();

The problem here is that the 'this' passed to MySvcWrap.doWork is the Window global object in this case. My intention is to pass function's execution context to myHandler.

What I have tried. If, instead of 'this', I pass an object, that works, e.g.:

function myHandler(data, ctx) {
    console.log('myHandler():  this.bar: ' + this.bar);  // <- no prob: this.bar 
    console.log(JSON.stringify(data));
}

function lookup() {
    var bar = 'food'; // local var
    MySvcWrap.doWork('thang', myHandler, {bar: bar}); // scope object is just object
}

I need someone to club me over the head here... when passing 'this' in my first case, of course this is the global scope (I am in a globally defined function)... My problem is that I thought when passing scope that I had access to locally defined variables and parameters w/in that context... Am I rocking my previous understanding of JS?? How to accomplish this task?

like image 556
Merl Avatar asked Feb 18 '11 17:02

Merl


2 Answers

btw, few words about scopes in your code:

function lookup() {
    var bar = 'food'; // local var
    MySvcWrap.doWork('thang', myHandler, this); // this here points to window object and not to the function scope
}

so it is the same as writing:

function lookup() {
    var bar = 'food'; // local var
    MySvcWrap.doWork('thang', myHandler, window); 
}

it is so because you define lookup function globally. inside doWork function, when you write this it points to MySvcWrap object (because that function is defined inside that object).

If your callback function has to see bar variable, it should be defined in the same scope, like that

MySvcWrap = {
    doWork: function(p1, callback, scope) {
        var result = {colors: ['red', 'green'], name:'Jones', what: p1};
        if (callback) {
            callback.call(scope||this,result, scope);
        } 
    }
}
function lookup() {
    var bar = 'food'; // local var
    MySvcWrap.doWork('thang', 
        function (data, ctx) {
            console.log('myHandler():  bar: ' + bar); 
            console.log(JSON.stringify(data));
        }, 
        this); // scope object is this
}

lookup();

in this case you send anonymous function as callback, it is defined inside lookup function so it has access to its local variables; my console shows me in this cae:

myHandler(): bar: food
{"colors":["red","green"],"name":"Jones","what":"thang"}

to make it easier to support, you can define myHandler inside lookup function:

function lookup() {
    var bar = 'food'; // local var
    var myHandler = function(data, ctx) {
        console.log('myHandler():  bar: ' + bar);
        console.log(JSON.stringify(data));
    };
    MySvcWrap.doWork('thang', myHandler, this); // scope object is this
}

On the other hand, why one function should have access to local variables of another function? Maybe it can be redesigned...

One more way to make your code working is to use anonymous function, instead of lookup (will work if you just declare and execute that function once):

(function() {
   var bar = 'food';

   function myHandler(data, ctx) {
       console.log('myHandler():  bar: ' + bar);  
       console.log(JSON.stringify(data));
    } 
    MySvcWrap = {
       doWork: function(p1, callback, scope) {
           var result = {colors: ['red', 'green'], name:'Jones', what: p1};
           if (callback) {
               callback.call(scope||this,result, scope);
           } 
       }
    }
    MySvcWrap.doWork('thang', myHandler, this);
  }
)();

result is the same, but no lookup function anymore...

And one more idea to make it working... Actually you need to define callback handler in the same scope where bar variable is defined, so it can be done a bit tricky, but just as alternative:

function myHandler(bar) { // actually in this case better to call it createHandler 
    return function(data, ctx) {
        console.log('myHandler():  bar: ' + bar); 
        console.log(JSON.stringify(data));
    } 
}
MySvcWrap = {
    doWork: function(p1, callback, scope) {
        var result = {colors: ['red', 'green'], name:'Jones', what: p1};
        if (callback) {
            callback.call(scope||this,result, scope);
        } 
    }
}
function lookup() {
    var bar = 'food'; // local var
    MySvcWrap.doWork('thang', myHandler(bar), this); // scope object is this
}

And few resources to read about JavaScript scoping and closure:

  1. Explaining JavaScript scope and closures
  2. Picking up Javascript - Closures and lexical scoping
  3. JavaScript: Advanced Scoping & Other Puzzles - very nice presentation about topic
like image 50
Maxym Avatar answered Oct 23 '22 08:10

Maxym


The code would work if it was structured like this.

MySvcWrap = {
    doWork: function(p1, callback, scope) {
        var result = {colors: ['red', 'green'], name:'Jones', what: p1};
        if (callback) {
            callback.call(scope||this,result, scope);
        } 
    }
}
function lookup() {
    var bar = 'food'; // local var

    function myHandler(data, ctx) {
        console.log('myHandler():  bar: ' + bar);  // <- prob: bar undefined 
        console.log(JSON.stringify(data));
    }

    MySvcWrap.doWork('thang', myHandler, this); // scope object is this
}

lookup();

The myHandler function can access the local variables of lookup as it is enclosed by it, thus a closure.

I'd probably try to achieve the same result, by structuring the code like this.

function myHandler(data, bar, ctx) {
    console.log('myHandler():  bar: ' + bar);  // <- prob: bar undefined 
    console.log(JSON.stringify(data));
}
MySvcWrap = {
    doWork: function(p1, callback) {
        var result = {colors: ['red', 'green'], name:'Jones', what: p1};
        if (callback) {
            callback(result);
        } 
    }
}
function lookup() {
    var bar = 'food'; // local var
    MySvcWrap.doWork('thang', myHandler.bind(this, bar)); // callback function is bound to the scope
}

lookup();

Rather than pass the scope, I use bind inside the lookup method. Using bind I can also add the local variables I wish to pass from that scope.

Of course as bind isn't available in older browsers, you either need to add it via a framework, or use one of the many little snippets of code that add the method if it doesn't exist.

like image 30
leebriggs Avatar answered Oct 23 '22 08:10

leebriggs