4

I'm trying to understand why in the following code I need Dragger.prototype.wrap and why I can't just use the event handling methods directly:

function Dragger(id) {
    this.isMouseDown = false;
    this.element = document.getElementById(id);
    this.element.onmousedown = this.wrap(this, "mouseDown");
}

Dragger.prototype.wrap = function(obj, method) {
    return function(event) {
        obj[method](event);
    }
}

Dragger.prototype.mouseDown = function(event) {
    this.oldMoveHandler = document.body.onmousemove;
    document.onmousemove = this.wrap(this, "mouseMove");
    this.oldUpHandler = document.body.onmousemove;
    document.onmouseup = this.wrap(this, "mouseUp");
    this.oldX = event.clientX;
    this.oldY = event.clientY;
    this.isMouseDown = true;
}

Dragger.prototype.mouseMove = function(event) {
    if (!this.isMouseDown) {
        return;
    }
    this.element.style.left = (this.element.offsetLeft
            + (event.clientX - this.oldX)) + "px";
    this.element.style.top = (this.element.offsetTop
            + (event.clientY - this.oldY)) + "px";
    this.oldX = event.clientX;
    this.oldY = event.clientY;
}

Dragger.prototype.mouseUp = function(event) {
    this.isMouseDown = false;
    document.onmousemove = this.oldMoveHandler;
    document.onmouseup = this.oldUpHandler;
}

I'm told it's because this changes without it, but I don't understand why this changes, why the wrap function prevents it from changing, and what this would change to without the wrap function.

Tony Stark
  • 24,588
  • 41
  • 96
  • 113
  • Your title implies a different question than the content of your question. Do you want to know about event handlers, the value of `this` in a given scope, or class definition with `prototype`? – Justin Johnson Nov 20 '09 at 06:14

2 Answers2

8

You need to wrap them because when a function is used as an event handler, the this keyword refers to the DOM element that triggered the event, and if you don't wrap it, you don't have access to the instance members of your Dragger object, like this.isMouseDown.

For example:

Let's say you have a button:

<input type="button" id="buttonId" value="Click me" />

And you have the following object:

var obj = {
  value: 'I am an object member',
  method: function () {
    alert(this.value);
  }
}

If you call:

obj.method();

You will see an alert with the text contained in the value member of the obj object ('I am an object member').

If you use the obj.method function as an event handler:

document.getElementById('buttonId').onclick = obj.method;

When the user clicks the button, it will alert 'Click me'.

Why? Because when the click event is fired, obj.method will be executed with the this keyword pointing to the DOM element, and it will alert 'Click me' because the button contains a value member.

You can check the above snippets running here.


For context enforcement, I always keep close a bind function:

// The .bind method from Prototype.js
if (!Function.prototype.bind) {
  Function.prototype.bind = function(){
    var fn = this, args = Array.prototype.slice.call(arguments),
             object = args.shift();
    return function(){
      return fn.apply(object,
        args.concat(Array.prototype.slice.call(arguments)));
    };
  };
}

It allows you to wrap any function, enforcing the context. As the first argument, it receives the object that will be used as this, and the rest of optional arguments, are the ones the wrapped functionenter code here will be called with.

In the button example we could use it as :

document.getElementById('buttonId').onclick = obj.method.bind(obj);

It's really helpful in a lot of situations and it will be introduced as part of ECMAScript 5.

Christian C. Salvadó
  • 807,428
  • 183
  • 922
  • 838
  • 3
    sorry, could you elaborate? i don't understand. – Tony Stark Nov 20 '09 at 06:01
  • ah, so the difference is that 'this' will refer to the DOM object itself, and not to the handler (and thus I couldn't access things like this.isMouseDown since a
    doesn't have isMouseDown but my class Dragger does?)
    – Tony Stark Nov 20 '09 at 06:14
  • @hatorade: Exactly, you got it. – Christian C. Salvadó Nov 20 '09 at 06:16
  • and it's because of closure that the method declared in the object gets the object 'this' because it keeps the environment it was made in? – Tony Stark Nov 20 '09 at 06:22
  • I think it may have been confusing to see the object literal instead of an instantiated object. – Justin Johnson Nov 20 '09 at 06:23
  • @hatorade Yes, closure wins again. – Justin Johnson Nov 20 '09 at 06:23
  • ooh ok i think i get it - the wrap takes 'this' as the object is declared (refers to the object) and creates the method mouseDown, and the wrap keeps 'this' referring to the object because of closure. then, since everything else is called through mouseDown, 'this' always refers to the same object that was initially referenced. correct? – Tony Stark Nov 20 '09 at 06:34
  • @hatorade: Correct, `wrap` returns a new function that remembers (thanks to the outer closure) `this` which it's being passed as the `obj` argument. – Christian C. Salvadó Nov 20 '09 at 06:45
3

CMS gave a good answer about the value of this in different contexts. But as a side note, here's a handy function that you can use to generalize the effect of Dragger.wrap (which is similar to dojo.hitch) if you're not using a library or using a library that doesn't have such a tool:

var lockContext = function(context, callback) {
    return function() {
        callback.apply(context, arguments);
    }
};
Community
  • 1
  • 1
Justin Johnson
  • 30,978
  • 7
  • 65
  • 89