Search code examples
javascriptcallbackclosuresthismodule-pattern

JS Module Pattern's public method as callback victim. (this-issue)


I spent the better part of the day reading about the module pattern and its 'this' scope. Eventually I found a work-around for my problem, although with a feeling there's a better way of doing things.

The actual code is >200 lines, but I've boiled it down to the following: objA has a method (publicA) that objB wants invoke by callback. The detail that complicates things is that publicA needs help from publicA_helper to do its job. (http://jsfiddle.net/qwNb6/2/)

var objA = function () {
    var privateA = "found";
    return {
        publicA: function () {
            console.log("privateA is " + this.publicA_helper());
        },
        publicA_helper: function () {
            return privateA;
        }
    };
}();

var objB = function () {
    return {
        callback: function (callback) {
            callback();
        }
    }
}();

objA.publicA(); // privateA is found
objB.callback(objA.publicA); // TypeError: Object [object global]

Fair enough – I've grasped that the caller's context tends to influence the value of 'this'. So I add measures to retain 'this' inside objA, of which none seems to work. I've tried the var objA = (){}.call({}) thingy, setting var self = this; (calling self.publicA_helper() accordingly). No luck.

Eventually, I added a private variable var self;, along with a public method:

init: function() {self = this;},

...and by making sure I call objA.init(); before passing objA.publicA to objB.callback, things actually work.

I cannot stress the immensity of the feeling that there's a better way of doing this. What am I missing?


Solution

  • I've tried the var objA = (){}.call({}) thingy,

    How? You want to use call on the callback that you want to invoke with a custom this, not on your module closure. It should be

    var objB = {
        callback: function (callback, context) {
            callback.call(context);
        }
    };
    
    objB.callback(objA.publicA, objA);
    

    I've tried setting var self = this;

    The self variable is supposed to be in a closure and point to the object on the methods are stored. That is only this when your module IEFE would be invoked on your module - it's not. Or if it was a constructor - it's not. You could change that with call as above:

    var objA = function () {
        var privateA = "found",
            self = this;
        this.publicA = function () {
            console.log("privateA is " + self.publicA_helper());
        };
        this.publicA_helper = function () {
            return privateA;
        };
        return this;
    }.call({});
    

    But that's ugly. In your case, the self variable simply needs to point to the object literal which you're returning as your module:

    var objA = function () {
        var privateA = "found",
            self;
        return self = {
            publicA: function () {
                console.log("privateA is " + self.publicA_helper());
            },
            publicA_helper: function () {
                return privateA;
            }
        };
    }();
    

    Btw, since you're creating a singleton you don't need an explicit self, you could just reference the variable that contains your module (as long as that doesn't change):

    var objA = function () {
        var privateA = "found";
        return {
            publicA: function () {
                console.log("privateA is " + objA.publicA_helper());
            },
            publicA_helper: function () {
                return privateA;
            }
        };
    }();
    

    Another method would be to simply make all functions private and then expose some of them - by referencing them local-scoped you will have no troubles.

    var objA = function () {
        var privateA = "found";
        function publicA() {
            console.log("privateA is " + helper());
        }
        function helper() {
            return privateA;
        }
        return self = {
            publicA: publicA,
            publicA_helper: helper // remove that line if you don't need to expose it
        };
    }();