DRobertE
DRobertE

Reputation: 3508

Javascript closure and "this"

My question is... in CallMeLaterTestObj function in the TestObj the "this" is the window object and not TestObj. How can I restructure this so that within the CallMeLater function I don't have to wrap the call function() { v.CallMeLaterTestObj(); } in a closure or using the bind function since it has limited support to newer browsers. Two objectives:

Upvotes: 0

Views: 158

Answers (2)

Jed Watson
Jed Watson

Reputation: 20378

I think what you're looking for is this:

function TestObj(value) {
    var _value = value;

    this.giveMe = function() {
        return _value;
    };

    this.callMeLaterTestObj = function() {
        console.log('I am ' + this.constructor.name + ' my value is ' + _value);
    };

    return this;
};

function callMeLater(v, i) {
    setTimeout(function() {
        v.callMeLaterTestObj();
    }, 10);    
}

var v1 = new TestObj(1);
var v2 = new TestObj(2);
var v3 = new TestObj(3);

console.log('V1= ' + v1.giveMe());
console.log('V2= ' + v2.giveMe());
console.log('V3= ' + v3.giveMe());
console.log('---');

callMeLater(v1, 1);
callMeLater(v2, 2);
callMeLater(v3, 3);​

To access constructor.name, you need to declare the function with function name() syntax, rather than var name = function() syntax.

To keep private variables and expose a public api, expose the public variables as properties of this in the function.

Be sure to return this from the constructor function to make it work.

It's also good practice to follow the naming convention of CamelCase for class names (of which TestObj is one) and lowerCamelCase for variables / methods / objects / etc. Helps keep things clear as to which variables are instances, and which are Classes.

Test and see the console output expected here.

note

Regarding wrapping v.callMeLaterTestObj() in a closure for the setTimeout, this technique is completely cross-browser compatible. You won't have any issues.

The bind method is newer, although there are many libraries that will shim that for you in older browsers. My personal favourite is underscore.

note 2

You can't call a method on an object in setTimeout without wrapping it in a closure somewhere, however if you want to you can abstract the closure in the Class without using a generic bind function (as provided by Underscore or jQuery and others) you can 'roll your own' in the Class like this:

function TestObj(value) {

    var _value = value;
    var _self = this;

    this.giveMe = function() {
        return _value;
    };

    this.callMeLaterTestObj = function() {
        console.log('I am ' + this.constructor.name + ' my value is ' + _value);
    };

    this.getBinding = function(method) {
        var _self = this;
        return function() {
            _self[method].apply(_self, arguments);
        };
    };

    return this;
};

function callMeLater(v, i) {
    setTimeout(v.getBinding('callMeLaterTestObj'), 10);    
}

var v1 = new TestObj(1);
var v2 = new TestObj(2);
var v3 = new TestObj(3);

console.log('V1= ' + v1.giveMe());
console.log('V2= ' + v2.giveMe());
console.log('V3= ' + v3.giveMe());
console.log('---');

callMeLater(v1, 1);
callMeLater(v2, 2);
callMeLater(v3, 3);​

explanation:

You need to use some sort of binding because, when you pass the method to setTimeout, you pass it by reference. So all setTimeout sees is a function - not the object it was on, which is why you lose the context of this.

Since setTimeout will therefore execute the function in the default scope - i.e. the browser window - you need a way to get this back, by reference, either through an inline anonymous function, or by returning a closure that uses the apply method to 'reset' this.

note 3

If you wanted to have your own bind method, and not include a library that provides it for you or include it in every class then you can use this one from Underscore, which defers to the native method in newer browsers:

function bind(func, context) {
  var bound, args;
  if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
  if (!_.isFunction(func)) throw new TypeError;
  args = slice.call(arguments, 2);
  return bound = function() {
    if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments)));
    ctor.prototype = func.prototype;
    var self = new ctor;
    var result = func.apply(self, args.concat(slice.call(arguments)));
    if (Object(result) === result) return result;
    return self;
  };
};

Then use it like this:

function callMeLater(v, i) {
    setTimeout(bind(v.callMeLaterTestObj, v), 10);    
}

This will work well in all browsers.

Upvotes: 1

Bergi
Bergi

Reputation: 664256

No, you can't. That's just the way to do it. Btw, you can easily shim the bind method so that it is available in older browsers, too.

An alternative would be to move the closure into the prototype method, if you know that you always will need to bind the actual function:

TestObj.prototype.getCallMeLaterTestObj = function () {
    var that = this;
    return function() {
        console.log('I am ' + that.constructor.name + ' my value is ' + that._value);
    };
};
setTimeout(v.getCallMeLaterTestObj(), 10);

Btw, your prototype has no constructor property so the log will not work as expected.

Your only chance is to avoid the this keyword entirely:

TestObj = function() {
    var privateVar = false; // these are private static
    function TestObj(value) {
        function giveMe() {
            return value;
        }
        function callMeLaterTestObj() {
            console.log('I am TestObj my value is ' + giveMe());
        }
        this._value = value;
        this.giveMe = giveMe;
        this.callMeLaterTestObj = callMeLaterTestObj;
        /* you could do this as well:
        return {
           _value: value,
           giveMe: giveMe,
           callMeLaterTestObj: callMeLaterTestObj
        }; */
    }
    return TestObj;
})();
var v = new TestObj;
setTimeout(v.callMeLater, 10);

But this is not very memory-efficient, as it does not use prototypical inheritance at all.

Upvotes: 1

Related Questions