最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

jquery - Correct way to bind "this" in JavaScript event callbacks? - Stack Overflow

programmeradmin6浏览0评论

I created a class called SearchBox to handle search interaction (delayed trigger, search on enter key press, preventing searches while one is active, synchronizing results when a search completes and the text has changed, etc.).

All the class methods are prototype methods, meant to be accessed via this. In the following code, assume p is the class's prototype.

p.registerListeners = function () {
    $(this.element).on('keypress', this.searchKeyPressed);
};

p.unregisterListeners = function () {
    $(this.element).off('keypress', this.searchKeyPressed);
};

That doesn't work, because when the keypress event calls the searchKeyPressed handler, it doesn't do so in the context of this. The only solution I can come up with is one that only modern browsers support, which is to bind the callback to this, which actually creates a new function. Since it creates a new function, I have to cache it in order to remove it later, since I have to pass the same reference to off that I passed to on.

Is there a better way to do it than this, or is this ok?

var boundKeyPressed;

p.registerListeners = function () {
    boundKeyPressed = this.searchKeyPressed.bind(this);
    $(this.element).on('keypress', boundKeyPressed);
};

p.unregisterListeners = function () {
    $(this.element).off('keypress', boundKeyPressed);
};

I thought that maybe jQuery.on would provide a way to do this event binding automatically, but instead it seems like it binds this to different things depending on how it's called. For example, when using on('eventname',instance.func), this is the "currentTarget" (not necessarily "target" in bubbling terms), whereas when using on('eventname','selector',instance.func), this refers to the element matching the selector. In either case, the func runs as though it has no relationship with instance.

I created a class called SearchBox to handle search interaction (delayed trigger, search on enter key press, preventing searches while one is active, synchronizing results when a search completes and the text has changed, etc.).

All the class methods are prototype methods, meant to be accessed via this. In the following code, assume p is the class's prototype.

p.registerListeners = function () {
    $(this.element).on('keypress', this.searchKeyPressed);
};

p.unregisterListeners = function () {
    $(this.element).off('keypress', this.searchKeyPressed);
};

That doesn't work, because when the keypress event calls the searchKeyPressed handler, it doesn't do so in the context of this. The only solution I can come up with is one that only modern browsers support, which is to bind the callback to this, which actually creates a new function. Since it creates a new function, I have to cache it in order to remove it later, since I have to pass the same reference to off that I passed to on.

Is there a better way to do it than this, or is this ok?

var boundKeyPressed;

p.registerListeners = function () {
    boundKeyPressed = this.searchKeyPressed.bind(this);
    $(this.element).on('keypress', boundKeyPressed);
};

p.unregisterListeners = function () {
    $(this.element).off('keypress', boundKeyPressed);
};

I thought that maybe jQuery.on would provide a way to do this event binding automatically, but instead it seems like it binds this to different things depending on how it's called. For example, when using on('eventname',instance.func), this is the "currentTarget" (not necessarily "target" in bubbling terms), whereas when using on('eventname','selector',instance.func), this refers to the element matching the selector. In either case, the func runs as though it has no relationship with instance.

Share Improve this question asked Dec 1, 2015 at 23:15 TriynkoTriynko 19.2k21 gold badges112 silver badges182 bronze badges 6
  • Missing AS3 methods where this was not an issue. Nothing like stepping 10 years backwards with JS, running into redundant syntax like 'this.method.bind(this)' (with limited support, mind you), where 'this.method' should suffice. – Triynko Commented Dec 1, 2015 at 23:20
  • 1 Have you tried passing this to the handlers as a parameter – Binvention Commented Dec 1, 2015 at 23:23
  • Because the boundkeypress runs a different function so the this parameter is not connected unless you pass it to it – Binvention Commented Dec 1, 2015 at 23:30
  • I can get the instance to the handler, sure, if I'm willing to do it by passing it through the event's data property, but that doesn't solve the problem that 'this' is still something other than the instance method's instance... because 'instance methods' don't really exist in JavaScript, unless you explicitly/redundantly bind it to this by calling 'this.method.bind(this)'. – Triynko Commented Dec 1, 2015 at 23:36
  • 1 @Triynko which actually does great things, in terms of allowing mixins, traits, partial-application; all of these things that "Enterprise" languages are scrambling to add in, with syntax a lot weirder than .bind. Also, come ES6, e => this.method(e); will work, come ES7, it looks like you can have this::method. – LetterEh Commented Dec 2, 2015 at 0:29
 |  Show 1 more comment

5 Answers 5

Reset to default 8

If you add a namespace to your events you can bind events and easily unbind them all at once.

To bind:

$(this.element).on('keypress.mynamespace', this.searchKeyPressed.bind(this));

To unbind:

$(this.element).off('.mynamespace');

First, unless you expect your page to be very long lived (or you're listening to keypresses with hundreds of things, which is a very big architecture problem), this isn't going to be much of an issue, where memory is concerned, even if phones had "keys".

Second, .bind has great support for all browsers less than half a decade old, and is dirt simple to polyfill.

Third: you're 100% right, that it's not cool to have to cache the function to be able to deal with it later, so let's do something about that.

There's a little known trick to addEventListener (and attachEvent), in that it happily supports objects, with handleEvent methods on them.

I don't use this for everything, as sometimes it's really just not worth it, but for game-making, I've used it for inputs, kind of like so:

class Input {
  constructor (events) {
    this.events = events || [];
  }

  handleEvent (e) {
    var input = this;
    var method = e.type;
    if (typeof input[method] === "function") {
      input.dispatchEvent(method, e);
    }
  }

  dispatchEvent (method, content) {
    var input = this;
    input[method](content);
  }

  listen (el, events) {
    var input = this;
    events = events || input.events;
    events.forEach(event => el.addEventListener(event, input));
    return this;
  }

  ignore (el, events) {
    var input = this;
    events = events || input.events;
    events.forEach(event => el.removeEventListener(event, input));
    return this;
  }
}


class Keyboard extends Input {
  constructor () {
    super(["keydown", "keyup"]);
    var keyboard = this;
    keyboard.keys = new Set();
  }

  press (key) { this.keys.add(key); }
  release (key) { this.keys.delete(key); }
  isPressed (key) { return this.keys.has(key); }

  keydown (e) {
    var key = e.keyCode;
    this.press(key);
  }

  keyup (e) {
    var key = e.keyCode;
    this.release(key);
  }
}

I could then:

var gameplayEvents = ["keyup", "keydown"];
var keyboard = new Keyboard();
keyboard.listen(canvas, gameplayEvents);

// ongameover
keyboard.ignore(canvas, gameplayEvents);

And if you'll note, it's all 100% pure JS. No jQuery, extJS, etc. And really, it's seriously not a lot more code, either. I could make it one object-literal, if I just needed one instance to handle mouseup and mousedown; really all I need is an object with a handleEvent, to become this inside of the handleEvent callback.

There's only one instance to worry about. I don't cache anything extra, if I need to unregister.

jQuery (and others) actually use this internally, to optimize the atrocious code which they're typically abused into producing.

Yes, perhaps I'm cheating by using ES6... ...but it's not necessary at all.

It's just more cheerful than what I used to do:

function Parent (args) { }
extend(Parent.prototype, { /*public-methods*/ });

function Child (args) {
  Parent.call(this);
  // construct
}
extend(
  Child.prototype,
  Parent.prototype,
  { /*public-override-methods*/ },
  { constructor: Child }
);

And again, there are lots of times when bind is 100% valid.
There's a proposal right now, for an ES7 version of bind, which would potentially produce the same value, every time it's called (if it goes through that way).

With the added benefit that the syntax allows for chaining all kinds of awesome things together, as well.

Instead of using bind, you could use the jQuery.proxy function to preserve context, which creates a wrapper function you can unbind:

jQuery.proxy has multiple variants, but for now the jQuery.proxy(function, context) is what you'll have to use:

p.registerListeners = function () {
    $(this.element).on('keypress', $.proxy(this.searchKeyPressed, this));
};

p.unregisterListeners = function () {
    $(this.element).off('keypress', $.proxy(this.searchKeyPressed, this));
};

Add bind(this) in the constructor - so you dont create new function every time you call .bind. bind creates new function every time you call it, so if you attach it with this: $el.on("click", this.handler.bind(this)), you cannot detach it with $el.off("click", this.handler.bind(this)) because the handler is not the same. (this.handler.bind(this) !== this.handler.bind(this)) If you save that reference to that binded function (like in the constructor this.handler = this.handler.bind(this)), then you can $el.on("click", this.handler) and $el.off("click", this.handler), because the handler is the same.

With this method, you are essentially overwriting that function for that instance. It will no longer call function like on prototype, but function like on that instnce.

function MyObject($el) {
    this.testValue = "I am really this!";
    this.$el = $el;
    this.onClick = this.onClick.bind(this);
    this.render();
}
MyObject.prototype.onClick = function(evt) {
    console.log(this.testValue); // logs "I am really this!"
}
MyObject.prototype.render = function() {
    var $a = $("<a>", {"text": "Click on me!"}).appendTo($el.empty());
    $a.on("click", this.onClick);
}

You can use a closure.

p.registerListeners = function() {
    var me = this;

    $(me.element).on('keypress', function() {
        me.searchKeyPressed.apply(me, arguments);
    });
};

Use apply to pass in the arguments.

发布评论

评论列表(0)

  1. 暂无评论