views:

136

answers:

3

Hello, I was toying (read: learning) around with Javascript and came across something to my understanding, seems very odd. It has to do with closures and a reference that seems to 'loose' its importance to the browser.

The browser I am using is Chromium 5.0.307.7.

Anyway, here's some code:

HTMLElement.prototype.writeInSteps = function() {
  var i = 0;
  var elem = this;
  var args = arguments;

  function step() {
    elem.innerHTML += args[i];

    if(i < args.length) {
      i += 1;
    } else {
      elem.innerHTML = "";
      i = 0;
    }


    setTimeout(step, 500);
  }

  step();
}

What happens here is that the first argument gets written to the correct HTMLElement, but all the ones after does not. What seems to happen is that after the first argument, the following arguments are written to some other element that is now being referenced by 'elem'.

I should also mention that, this only seems to happen when I write something directly after calling this function, like this:

div.writeInSteps("This", " is", " not", " working");
$id("body").innerHTML += "Doh!";

If I refrain from writing anything after calling this function, it seems to work ok.

If I instead change the above code to:

HTMLElement.prototype.writeInSteps = function() {
  var i = 0;
  var e = this.id;
  var args = arguments;

  function step() {
    var elem = $id(e);
    elem.innerHTML += args[i];

    if(i < args.length) {
      i += 1;
    } else {
      elem.innerHTML = "";
      i = 0;
    }


    setTimeout(step, 500);
  }

  step();
}

Everything is dandy. My question is, what's really happening behind the scenes in the first version?

EDIT: Updated with requested details about "...write something directly after..." and browser usage as requested by ntownsend. Bryan Matthews, I'm not sure how to provide a test page without making this question overly cluttered though.

+3  A: 

I suspect this is a DOM issue, not a JavaScript issue.

My guess is that something's mutating an ancestor of the element to which you're trying to write in steps. For example, if innerHTML of the element's parent is set (even to the exact same string, I think), the element reference you have will be to an element that's no longer in the DOM. Re-getting the element by ID each time would work around that problem.

Jeff Walden
+1 My guess too. Setting innerHTML maybe "optimized" to recreating the element, thus invalidating references.
Tomas
There's nothing optimised about it, setting `innerHTML` will **always** destroy every descendant Node and replace them with new nodes from markup, losing JavaScript references, properties and event handlers in the process. This is why you should **never** use `innerHTML+=` to add content.
bobince
What should I use to add content then?
jimka
This is really a fundamental issue inherent in modifying an element in the DOM, and one of its descendants, near-simultaneously (at least in an unsynchronized manner). Either you should not do that, or if you find it necessary to do so (I tend to think it should be possible to avoid such situations, but it might not always be easier to do so), you shouldn't rely on pre-computed references because they might have gone stale and no longer refer to the intended nodes in the DOM.
Jeff Walden
Jeff, while I understand the problem, can you more in-depth explain what happens when you write to innerHTML, maybe suggest appending text nodes instead of editing the innerHTML property directly?
jimka
What happens (maybe, I don't actually know the precise semantic ordering of steps involved in setting `innerHTML`) when setting `e.innerHTML`: 1. The set string is parsed as though by a parser which had parsed a token stream of everything previously in the document, generating a list of nodes -- or a parse error. 2. All `e`'s child nodes are removed. 3. All the generated nodes are added as children of `e`.Cached references thus refer to nodes removed from `e`, not to the nodes actually there, so nothing visibly happens. And yes, the fix is to modify existing nodes or append new ones.
Jeff Walden
A: 

my guess is, execution of setTimeout(step, 500); callback has very little idea who this is - whether you're calling current element's step or maybe HTMLElement.prototype.writeInSteps.step() ?

Try your second code on two different elements simultaneously, (so that second writeInSteps comes before first timeout). I'm pretty sure it won't do quite what you expect.

Of course Jeff can be right too. innerHTML is read-only by specification, and writing to it may or may not rebuild the whole tree (and kill all the references).

SF.
Though, this wouldn't explain why his second example worked.
Mike Sherov
No, `step` refers unambiguously to the function defined in the current call to `writeInSteps` -- it's a local binding created anew every time `writeInSteps` is called. As `step` never refers to `this`, either, there shouldn't be a problem passing `step` directly to `setTimeout`.
Jeff Walden
-1 This is not the problem. The whole idea of using the elem closure is to preserve a reference to this
Tomas
A: 

If you're replacing the innerHTML of an ancestor of elem (the body element, as per your example), then elem no longer exists. When step is after the original elem is destroyed, what elem is referencing is going to be something else.

The correct thing for the browser to do should probably be to remove the elem reference, but it doesn't look like it's doing that.

ntownsend