JAVA & XML & JAVASCRIPT & AJAX & CSS

Web 2.0 技术储备............

  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  77 随笔 :: 17 文章 :: 116 评论 :: 0 Trackbacks

Object-Oriented Event Listening through Partial Application in JavaScript

Daniel Brockman
July 23, 2004
Revised March 21, 2006

Abstract

A general solution to the simple but common problem of attaching event listeners to HTML elements owned by object-oriented code is presented. The solution does not rely on the questionable method of injecting hidden backreferences to the application logic into the HTML elements; rather, the listeners are connected to the application logic in a purely hygeinic fashion through the use of lexical closures.

Appreciating the need for method references

The following code does not do what the programmer intended it to:

function GreetingButton(greeting) {
    this.greeting = greeting;
    this.element = document.createElement("button");
    this.element.appendChild(document.createTextNode("Receive Greeting"));
    this.element.onclick = this.greet;
}

GreetingButton.prototype.greet = function () {
    alert(this.greeting);
};

onload = function () {
    gb = new GreetingButton("Hello!");
    document.body.appendChild(gb.element);
    gb.greet();
};

When the button labelled Receive Greeting is clicked, the string undefined will be alerted instead of the intended Hello!. The reason is that this in the expression alert(this.greeting) will refer to the HTML button that was clicked—notgb, the GreetingButton object.

The function GreetingButton::greet, like any other method, has no way of knowing that we consider it to be a “method” of some object. Indeed, in JavaScript, a single function can “be a method of” zero, one, two, or any number of objects.

Specifically, a function f is bound to an object o—i.e., this refers to o in f’s scope—only while f is being invoked on o (usually as o.f(...)). The following example should illustrate this nicely:

function f() { alert(this); };

var foo = "foo";
var bar = "bar";
foo.f = bar.f = f;

After running this code, foo.f() will cause foo to be alerted, and bar.f() will cause bar to be alerted, even though the same function is being invoked in both cases.

If you were to invoke f simply as f(), this in the expression alert(this) would refer to the global object (also accessible as window in the browser API), so a string representing the global object—such as [object]—would be alerted. But let’s not dwell on this.

To side-step the problem, you might consider doing this:

function GreetingButton(greeting) {
    this.greeting = greeting;
    this.element = document.createElement("button");
    this.element.appendChild(document.createTextNode("Receive Greeting"));
    this.element.greetingButton = this;
    this.element.onclick = this.greet;
}

GreetingButton.prototype.greet = function () {
    alert(this.greetingButton.greeting);
};

onload = function () {
    gb = new GreetingButton("Hello!");
    document.body.appendChild(gb.element);
    gb.greet();
};

This code works as intended, but the solution is inelegant. While it is unlikely that the name greetingButton will ever conflict with anything, you’re still essentially polluting someone else’s namespace.

Another problem with this approach is that it theoretically breaks encapsulation, since you are in fact setting up a backdoor to your logic. Anyone with access to the HTML document that your element is attached to could aquire a reference to your backdoored element, and, by extension, your private data. This will probably not present itself as a practical problem to anyone, but it necessarily remains a fundamental inelegancy.

Binding a variable to the value of this and then closing over it is a superior approach:

function GreetingButton(greeting) {
    this.greeting = greeting;
    this.element = document.createElement("button");
    this.element.appendChild(document.createTextNode("Receive Greeting"));
    var greetingButton = this;    this.element.onclick = this.greet;    this.element.onclick = function () {
        greetingButton.greet();
    };
}

GreetingButton.prototype.greet = function () {
    alert(this.greeting);
};

onload = function () {
    gb = new GreetingButton("Hello!");
    document.body.appendChild(gb.element);
    gb.greet();
};

Essentially, a closure, or lexical closure, is a function f coupled with a snapshot of its lexical environment (i.e., the scopes that contain the non-local variable bindings used in the body of f).

Hence, closing over some variable v means creating a closure that refers to v.

Some people use the term “closure” to describe any function that has an outer lexical scope. Since this is a property shared by virtually all functions in most languages (if nothing else, most functions reside in some kind of global lexical scope), I try to use the term only when f has the following characteristics:

  • The body of f appears within the body of another function g.
  • Some variables that are bound in g’s local scope are referred to in the body of f.
  • The function f can be called by code that does not appear within the body of g, making f a kind of interface between its environment and external code.

The canonical example of a closure is the counter:

function makeCounter(start) {
    var current = start;
    return function () { return current++; };
}

Here, f is the the anonymous function that appears within the body of makeCounter (our g). The variable current is bound in makeCounter’s local scope and referred to in the body of the anonymous function. Finally, our f can be called by code that does not appear within the body of makeCounter because it is returned to the outside code.

You might wonder why we can’t just close over this, instead of declaring a proxy variable and closing over that.

						
								var greetingButton = this;

						this.onclick = function () {
    greetingButton.greet();    this.greet();
};

The reason why this approach won’t work is that the function will no longer become a closure, because it no longer refers to any variables. Specifically, this is not considered to be a variable; rather, it is at best a pseudovariable, with the implications that it can neither be closed over nor assigned to.

Refactoring this a bit, we get the following:

function GreetingButton(greeting) {
    this.greeting = greeting;
    this.element = document.createElement("button");
    this.element.appendChild(document.createTextNode("Receive Greeting"));
    this.element.onclick = function () {
        greetingButton.greet();
    };    this.element.onclick = createMethodReference(this, "greet");
}

GreetingButton.prototype.greet = function () {
    alert(this.greeting);
};

function createMethodReference(object, methodName) {
    return function () {
        object[methodName]();
    };
};

onload = function () {
    gb = new GreetingButton("Hello!");
    document.body.appendChild(gb.element);
    gb.greet();
};

And thus we arrive at the essential point of this article. We have just discovered a basic method for “welding” a function and an object, forming what we have been calling a method reference.

I call this form of event listening “object-oriented,” because instead of attaching callback functions, you attach callback methods. When doing object-oriented programming, an equivalent or similar facility is nearly essential.

I say “through partial application” because partial application—i.e., informally, calling a function with only a subset of its arguments and getting back the remainder—is actually what createMethodReference does, or at least something very close.

The concept of partial application can be visualized by thinking of a function as a box with a number of empty slots—one for each argument that the function expects. Partially applying the function to some arguments, then, amounts to plugging just those arguments into the appropriate slots, producing a new box with fewer empty slots.

Of course, this process can be repeated until there are no more empty slots, at which point one of two things happens:

the normal semantics
The function has really been completely applied, so its result is returned. This is what would happen in, e.g., O’Caml or Haskell, which are two of the many languages with deeply built-in support for partial application.
our semantics
We are left with something that Schemers and other socially maladjusted people like to call a thunk—i.e., a zero-argument closure that you need to call one last time to actually execute it. This is how our method references work, and the browser will often be the one performing this last call.

As to how this relates to our code, note that a method can be thought of simply as a function taking an extra first argument: the receiving object. Thus, when we call createMethodReference(o, "m"), we are essentially just plugging o into the first slot (or, perhaps more accurately, the “zeroth” slot) of the function o.m—partial application.

Letting external arguments pass through method references

When you click a button on a web page, the browser calls the event listener referred to by the element’s onclick property. It sends one argument to the event listener: the event object, which says things like the exact coordinates at which the click occured.

However, using the above definition of createMethodReference, an event listener method has no way of accessing the event object. So we would like the method reference to pass on the arguments it receives—the event object, in this case—to the method it references.

function GreetingButton(greeting) {
    this.greeting = greeting;
    this.element = document.createElement("button");
    this.element.appendChild(document.createTextNode("Receive Greeting"));
    this.element.onclick = createMethodReference(this, "greet");
}

GreetingButton.prototype.greet = function (event) {
    alert(this.greeting);
};

function createMethodReference(object, methodName) {
    return function () {
        object[methodName]();        object[methodName].apply(object, arguments);
    };
};

onload = function () {
    gb = new GreetingButton("Hello!");
    document.body.appendChild(gb.element);
    gb.greet();
};

The Function::apply method is analogous to the apply functions found in Common Lisp and Scheme, except that the this argument is given separately.

If that doesn’t make you any wiser, here’s another way of looking at it: The following

func.apply(obj, args);

is roughly equivalent to the following,

obj.___tmp___ = func;
obj.___tmp___(args[0], args[1], args[2], ...);

except that

  • the former version doesn’t conflict with perfectly legitimate uses of the property ___tmp___; and
  • the former version actually uses the elements of args as arguments (the latter version would in fact give a syntax error).

It might help to note that o.m.apply(o, [a, b, c]) is exactly equal to o.m(a, b, c) (except of course that the latter version evaluates o only once).

Note that arguments in the expression apply(object, arguments) refers to the arguments of the inner, anonymous function—not the createMethodReference function itself.

But wait. In making the GreetingButton::greet method accept an event argument, we have introduced a subtle inconsistency: Users are also expected to call this method, and they will not be passing any event objects. To clarify the code, we should add some indirection:

function GreetingButton(greeting) {
    this.greeting = greeting;
    this.element = document.createElement("button");
    this.element.appendChild(document.createTextNode("Receive Greeting"));
    this.element.onclick = createMethodReference(this, "greet");    this.element.onclick = createMethodReference(this, "buttonClicked");
}

GreetingButton.prototype.buttonClicked = function (event) {
    this.greet();
};

GreetingButton.prototype.greet = function (event) {
    alert(this.greeting);
};

function createMethodReference(object, methodName) {
    return function () {
        object[methodName].apply(object, arguments);
    };
};

onload = function () {
    gb = new GreetingButton("Hello!");
    document.body.appendChild(gb.element);
    gb.greet();
};

Taking special measures for event listeners

If you want to be compatible with Internet Explorer (as of 2005), you usually have to put this code at the top of every event listener:

event = event || window.event;

Note that we could say that more succintly with a logical assignment operator if we had ECMAScript 4 (also known as JavaScript 2.0).

To avoid cluttering up your code with such compatibility cruft, you can pull this logic into the function that creates the event listener method references:

function createEventListenerMethodReference(object, methodName) {
    return function (event) {
        object[methodName].call(object, event || window.event);
    };
}

(Note that in this case we assume that the method is always interested in exactly one argument, so we do not have to use the Function::apply method to pass arguments.)

The Function::call method is to Common Lisp’s funcall as Function::apply is to Common Lisp’s and Scheme’s apply.

It’s just like Function::apply, except that instead of taking an array of arguments with which to call the function, it takes the actual arguments. Thus, f.call(o, a1, a2, a3) is exactly the same as f.apply(o, [a1, a2, a3]).

Making method references not look like eval

You might want to be able to pass any function to createMethodReference instead of just a method name. That way, you can create method references to functions which aren’t really “methods” of the object in question (you might, for instance, be participating in an obfuscated JavaScript contest).

function createMethodReference(object, methodName) {
    if (!(method instanceof Function))
        method = object[method];

    return function () {
        method.apply(object, arguments);
    };
};

But perhaps more convincing is the fact that we no longer have to deal with strings whose content are more or less code:

				
						this.element.onclick = createMethodReference(this, "buttonClicked");
				
				
						this.element.onclick = createMethodReference(this, this.buttonClicked);
				
		

Giving method references a respectable look

The name createMethodReference is clearly long and awkward. We can improve on it by stealing a better name from Ruby, in which you can do the equivalent of createMethodReference by first obtaining an UnboundMethod (which is a roundabout way of saying “function”), and then binding it to some object. This is the terminology we’re looking for, so we’ll just rename our function to bind and make it a method of the Function class.

Function.prototype.bind = function (object) {
    var method = this;
    return function () {
        method.apply(object, arguments);
    };
}

Note again how we had to create a variable binding in order to close over the value of this. I said before that this is a “pseudovariable” and that that’s why you can’t close over it. A better explanation is that each time a function is called, a new this binding is created. In pseudocode, it works as follows (except that you still cannot assign to this):

function a() {
    var this = <object 1>;
    function b() {
        var this = <object 2>;
    }
}

So the above Function::bind works like this:

Function.prototype.bind = function (object) {
    var this = <object 1>;
    var method = this;
    return function () {
        var this = <object 2>;
        method.apply(object, arguments);
    };
}

Clearly, it wouldn’t do the same thing if it looked like this:

Function.prototype.bind = function (object) {
    var this = <object 1>;    var method = this;
    return function () {
        var this = <object 2>;        method.apply(object, arguments);        this.apply(object, arguments);
    };
}

Now we can use it like this:

				
						this.element.onclick = createMethodReference(this, this.buttonClicked);
				
				
						this.element.onclick = this.buttonClicked.bind(this);
				
		

Of course, you can do the same with the Internet Explorer-compatible event listener method reference creator. The obvious name would be bindAsEventListener, but let’s drop the “as”, as it’s only making the name more awkward.

Function.prototype.bindEventListener = function (object) {
    var method = this;
    return function (event) {
        method.call(object, event || window.event);
    };
}

More partial application (less-partial application)

We can already do partial application on the implicit this argument, but we can just as easily allow for more general partial application:

				
						// Some browsers don't like expressions such as foo.concat(arguments)
// or arguments.slice(1), due to a kind of reverse duck typing:
// an argument object looks like a duck and walks like a duck,
// but it isn't really a duck and it won't quack like one.
function entries(collection) {
    var result = [];  // This is our real duck.

    for (var i = 0; i < collection.length; i++)
        result.push(collection[i]);

    return result;
}
				

Function.prototype.bind = function (object) {
    var method = this;
    var preappliedArguments = entries(arguments).slice(1);
    return function () {
        method.apply(object, arguments);        var newArguments = entries(arguments);
        method.apply(object, oldArguments.concat(newArguments));
    };
}

With this definition of Function::bind, a call such as f.bind(o, 1, 2)(3, 4) will be rendered as f.call(o, 1, 2, 3, 4). The corresponding redefinition of Function::bindEventListener

Function.prototype.bindEventListener = function (object) {
    var method = this;
    var oldArguments = entries(arguments).slice(1);
    return function (event) {
        method.call(object, event || window.event);        method.apply(object, event || window.event, oldArguments);
    };
}

will cause f.bindEventListener(o, "moomin")("snufkin") to be rendered as f.call(o, event, "moomin", "snufkin"). This is handy whenever you want to attach an event listener in a certain “mode”. For example, you might have a grid of cells, each of which should react to click events.

function GridWidget(width, height) {

    // Create elements and populate cell array here.

    for (var x = 0; x < width; x++)
        for (var y = 0; y < height; y++)
            cells[x][y].element.onclick =
                this.cellClicked.bindEventListener(this);
}

GridWidget.prototype.cellClicked = function (event) {
    alert("I have no idea which cell you just clicked.");
};

As the message says, it is not obvious how to find out which one of the width × height different cells generated the click event, because all cells trigger the same event listener. It would clearly be ridiculous to attempt to manually define a separate event listener function for each cell, especially since the number of cells may very well be variable.

The solution, of course, is declaring the event listener to take two additional arguments giving the cell coordinates, and using partial application to plug these values in during object initialization.

function GridWidget(width, height) {

    // Create elements and populate cell array here.

    for (var x = 0; x < width; x++)
        for (var y = 0; y < height; y++)
            cells[x][y].element.onclick =
                this.cellClicked.bindEventListener(this, x, y);
}

GridWidget.prototype.cellClicked = function (event, x, y) {
    alert("I have no idea which cell you just clicked.");    alert("You clicked the cell at (" + x + ", " + y + ")!");
    this.cells[x][y].frobnicate();
};

Finally, it is useful to make the method references to pass on any values that the real methods return.

Function.prototype.bind = function (object) {
    var method = this;
    var oldArguments = entries(arguments).slice(1);
    return function () {
        var newArguments = entries(arguments);
        return method.apply(object, oldArguments.concat(newArguments));
    };
}

Function.prototype.bindEventListener = function (object) {
    var method = this;
    var oldArguments = entries(arguments).slice(1);
    return function (event) {
        return method.apply(object, event || window.event, oldArguments));
    };
}

For example, it is often necessary to return false from an event listener to signal that the default action should be suppressed.

Acknowledgements

I want to thank the following people for their help in writing and revising this article.

My dear friend Peter Wängelin provided the initial motivation for the article by asking some good questions that I felt deserved some good, written answers.

Sam Stephenson promoted the article by linking to it from his website and suggesting to people that they read it.

Mathieu van Loon corrected an error in the definitions of Function::bind and Function::bindEventListener, and suggested that method references should pass on the return values of the underlying methods.

Chih-Chao Lam pointed out the fact that argument objects cannot portably be used as arrays.

At least a few other people have linked to the article from various places on the web, and I want to thank them too.

Copying

Copyright © 2004, 2005, 2006 Daniel Brockman.

Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.2 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. Copies of the GNU Free Documentation License can be obtained at the following URL: http://www.gnu.org/licenses/

posted on 2006-04-26 19:37 Web 2.0 技术资源 阅读(1071) 评论(2)  编辑  收藏 所属分类: Javascript

评论

# aezlvbmu 2008-04-07 15:38 aezlvbmu
wdpadptx http://cpocbhac.com vqcrerrs mfftqfvc  回复  更多评论
  

# re: Object-Oriented Event Listening through Partial Application in JavaScript 2010-12-13 13:48 seek
Thanks very much!
that's what i really need!  回复  更多评论
  


只有注册用户登录后才能发表评论。


网站导航: