Binding “this” when passing an object's member

2019-07-19 03:34发布

问题:

I had a "class" defined and was making only one instance of it. The instance possessed a member function that would end up being passed around (it's a mouse handler, but that's not important). Since I would only ever make one instance of my "class", I decided to rewrite it as a singleton by using an object literal.

So I have

var mySingleton = {
    theObjects : [];
}

mySingleton.mouseHandler = (function() {
    var that = this;
    return function (e) {
        for (var indx = 0; indx < that.theObjects.length; indx++) {
            // do something to that.theObjects[indx];
        }
    }
}());

mySingleton.addObject = function(newObj) {
    this.theObjects.push(newObj);
}

However, when I try to use this code (after adding a few objects), I keep getting an that.theObjects is undefined error. It's referring to the line in the for loop.

回答1:

Update for 2015 – Use Function.bind() to specify the value of this within the function. Then, instead of using that, you can use this.

mySingleton.mouseHandler = function (e) {
    for (var indx = 0; indx < this.theObjects.length; indx++) {
        // do something to this.theObjects[indx];
    }
}.bind(mySingleton);

This doesn't work if you want mouseHandler to have the context of the 'moused' element. For that, use my original answer below.

If you need to support IE8 or (heaven forbid) earlier, you'll need to use a polyfill.


Since you are calling the function that creates mouseHandler immediately, it is run in the context of window, not mySingleton. So that refers to window. Instead of calling it immediately, just change it to a method so that it runs in the context of mySingleton:

mySingleton.getMouseHandler = function() {
    var that = this;
    return function() { ... };
};
myElement.onclick = mySingleton.getMouseHandler();

Of course, since you are already using a singleton, you can just reference it directly. In your click handler, instead of checking that.theObjects, check mySingleton.theObjects. Or, in mouseHandler change var that = this to var that = mySingleton.

Edit: Or, pass the context to your anonymous function when you call it:

mySingleton.mouseHandler = (function() {
    var that = this;
    return function (e) {
        for (var indx = 0; indx < that.theObjects.length; indx++) {
            // do something to that.theObjects[indx];
        }
    }
}).call(mySingleton);


回答2:

There are a few popular ways to do this. First, super-simple solution is just reference mySingleton directly and bypass the confusion associated with this. Instead of that.theObjects just do mySingleton.theObjects and move on with your life and things will work fine.

However, there is a common pattern to do this binding. Here's how underscore.js does it

Check out the annoted source to underscore, where you will find this

 _.bind = function(func, obj) {
    if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
    var args = slice.call(arguments, 2);
    return function() {
      return func.apply(obj, args.concat(slice.call(arguments)));
    };
  };


回答3:

The other answers here so far are also correct. Providing my viewpoint here in case it helps.

The key to understanding why the code doesn't behave as you expect requires understanding how this works in JavaScript. The problem is that this depends on how the function is called.

First, if you call the function in the method style, this is what you'd expect:

mySingleton.mouseHandler(); // this === mySingleton

If you attach the function to something esle, that works too.

var anotherSingleton = {};
anotherSingleton.foo = mySingleton.mouseHandler;
anotherSingleton.foo(); // this === anotherSingleton

If you detach the function, this becomes the global scope object (window)

var foo = mySingleton.mouseHandler;
foo(); // this === window

And finally, you can force this to be something else using call or apply:

var randomThingy = {};
mySingleton.mouseHandler.call(randomThingy); // this === randomThingy

The takeaway is that this is determined at runtime based on the context of how the function was called. Often, frameworks that allow you to make "classes" abstract these details from you by implicitly applying the bind pattern on your behalf. This is why it used to work, and no longer does.

As others have mentioned, you can change your handler to reference the variable by its scoped name (mySingleton) or otherwise bind it as discussed.

Here's an article I wrote on the subject a few years ago that goes into more detail: http://trephine.org/t/index.php?title=Understanding_JavaScript%27s_this_keyword

Hope this helps!