views:

221

answers:

1

I ran into a problem in IE8 today (Note that I only need to support IE) that I can't seem to explain: detachEvent wouldn't work when using a named anonymous function handler.

document.getElementById('iframeid').attachEvent("onreadystatechange", function onIframeReadyStateChange() {
    if (event.srcElement.readyState != "complete") { return; }

    event.srcElement.detachEvent("onreadystatechange", onIframeReadyStateChange); 

    // code here was running every time my iframe's readyState 
    // changed to "complete" instead of only the first time
});

I eventually figured out that changing onIframeReadyStateChange to use arguments.callee (which I normally avoid) instead solved the issue:

document.getElementById('iframeid').attachEvent("onreadystatechange", function () {
    if (event.srcElement.readyState != "complete") { return; }

    event.srcElement.detachEvent("onreadystatechange", arguments.callee);    

    // code here now runs only once no matter how many times the 
    // iframe's readyState changes to "complete"
});

What gives?! Shouldn't the first snippet work fine?

+1  A: 

Shouldn't the first snippet work fine?

Yes, arguably it should. But it doesn't. :-) Fortunately there's an easy workaround (and a better one than arguments.callee, which has issues [see below]).

The problem

The problem is that named function expressions (NFEs, which is what you have there) do not work correctly in JScript (IE) or several other implementations out in the wild. Yuriy Zaytsev (kangax) did a thorough investigation of NFEs and wrote up this useful article about them.

A named function expression is where you give a function a name and use the function statement as a right-hand value (e.g., the right-hand part of an assignment, or passing it into a function like attachEvent), like this:

var x = function foo() { /* ... */ };

That's a function expression, and the function is named. Arguably it should work, but in many implementations in the wild, including IE's JScript, it doesn't. Named functions work, and anonymous function expressions work, but not named function expressions. (Edit I shouldn't have said don't work, because in some ways they do. I should have said don't work properly; more in Yuriy's article and my answer to your follow-up question.)

The solution

Instead you have to do this:

var x = foo;
function foo() { /* ... */ };

...which does, after all, come to the same thing.

So in your case, simply do this:

document.getElementById('iframeid').attachEvent("onreadystatechange", onIframeReadyStateChange);
function onIframeReadyStateChange() {
    if (event.srcElement.readyState != "complete") { return; }

    event.srcElement.detachEvent("onreadystatechange", onIframeReadyStateChange);

    // code here was running every time my iframe's readyState
    // changed to "complete" instead of only the first time
}

That has the same effect as what you were trying to do, but without running into implementation problems.

The problem with arguments.callee

(This is slightly off-topic, but...) You're right to avoid using arguments.callee. In most implementations, using it carries a massive performance overhead, slowing down the function call by an order of magnitude (yes, really; and no, I don't know why). It's also disallowed in the new "strict mode" of ECMAScript 5 (and "strict mode" is mostly a good thing).

T.J. Crowder
Do you know of any good articles covering the performance overhead of accessing `arguments.callee`?
Roatin Marth
@Roatin: No, and I'd like to see one too. Maybe I'll write one. When I was proposing a more efficient means of doing supercalls to the Prototype team (http://groups.google.com/group/prototype-core/browse_thread/thread/db9ccdaae4f7f705/b5f576581ed274cd), kangax pointed out to me that it's slow, and when I tested it I was blown away. Chrome had the most overhead in my tests at that time, IE the least. (It should be said that Chrome at its slowest was faster than IE at its fastest; it's just the *relative* change was bigger in Chrome.)
T.J. Crowder
@Roatin: Haven't read it through yet, but this seems to cover it: http://webreflection.blogspot.com/2009/06/es5-arguments-and-callee-i-was-wrong.html In my tests for the Prototype thing, I found two layers of slowness: The first was if you used `arguments` at all (which isn't too surprising, given that `arguments` is a very weird thing -- it's "live", for one thing, if you have a named argument and assign a new value to it, its entry in the `arguments` array automagically updates; and vice-versa), and then a second slowdown if you actually used `callee`.
T.J. Crowder
@T.J.: Thanks for the article. Favorited...
Roatin Marth
@T.J.: I posted a follow-up question at http://www.stackoverflow.com/questions/2679657/named-function-expressions-in-ie-part-2
Polshgiant
How did this not have any vote-ups? Awesome answer.
mwilcox