Monday, May 25, 2015

Do Browsers Create Closures? Sometimes and Its Ugly.

While trying to simplify and extract the crucial binding bits for the Chakra script engine and EdgeHTML engine, I've come head to head with a complicated set of features in my code. These features are trying to add DOM objects to the scope chain for a special classification of functions that are defined the way they are purely due to historical reasons. We often find that only legacy compatibility can bring out such nastiness.

The HTML 5 functionality that I'm referring to is 7.1.5.1 in the HTML 5.1 nightly. This section is on event handler attributes. By themselves they aren't scary. In fact, setting the onclick property of an element is little different from a script engine binding perspective than using addEventListener. They both take an already compiled function whose environment has already been set up and use it as a callback.

Let's combine them with another concept, the internal raw uncompiled handler, and now you are about to see a train wreck. What we have is a new and special way to construct functions whose environment blocks are not the same as normal JavaScript functions. Instead of using normal closure rules to determine how variable names are hooked up, we get to instead inject additional objects like the document, a form and even the element into the scope chain. This can be quite counter intuitive since most developers are likely to think that a markup inline handler and setting the attribute handler to a function are going to do the same thing. They don't, and thus the train wreck begins.

TL;DR; You can still vote for F12 to support visualization of this strange behavior on User Voice.

Element Lexical Scopes

Okay, so let's look at the most basic of lexical scope problems. We'll have a button, on which we'll put some variables. We'll establish an onclick handler using JavaScript syntax. And then we'll access our variables by using the this variable. I've also put in a naked access which shows that our default lexical scope is Closure followed by window (Global).


We will do the same thing using an inline event handler. This time, our variable1 resolves to our button since the element was added to the lexical scope per the rules on creating a function from the markup's inline source text. I've also shown that a callout function doesn't inherit these scopes. This is most likely why people don't write code to rely on this behavior since inline event handlers are usually function calls to another method. This strips away a lot of the magic.


Form Lexical Scopes

If an element happens to have a form owner then we inject that into the scope chain as well. This is pretty important, since forms have a lot of special semantics. For instance, they have their own custom namespace that allows you to find elements owned by the form. When dealing with form instances this allows you to get to form children by specifying an ordinal index or the name/id of the element depending on the promotion rules.

An element can even be outside of a form, but also associated with it through the form attribute. This attribute will take the id of the form you want to bind to and now voila you are a member of that forms submit items collections. This technique is so strange and obscure it can even break you out of your containing form and put you in a different once. I'm not sure you'd ever want/need to do that, but if you want to, you certainly can.

The form gets injected between your element and document. This means you now have 3 new scopes, not normally available to you, in your namespace lookup. The following code snippet exploits all of these behaviors for you so you can play around with them. In this case I'm not showing the fallback to a normal property handle or addEventListener, but note the scopes melt away as you would expect in those cases.


Document Lexical Scopes vs Global Scope

I wanted to exploit a situation where you would get a different value if you used markup inline event handlers versus the property based event handler. The place where this can bite you the easiest is when dealing with differences between the document implicit namespace and the window frames namespace. Here are how the collections break down and I'm also giving them names very close to what we have in our sources:

  • window - frames collection - Returns IFRAME and FRAME elements. When indexed returns their WindowProxy of type [object Window].
  • window - window collection - All named items in the document. When indexed returns them as an element. Prior to EdgeHTML we did not count objects in forms. For WebKit Interop EdgeHTML now counts all elements, regardless of whether or not they are in forms.
  • document - navigator document collection - Can you guess this one is named after Netscape Navigator ;-) This is another smorgasbord collection of random stuff. Images, iframes, objects, embeds, images... The HTML 5.1 Nightly has the write-up on the specific conditions of this collection at the bottom of the document object section.
When looking up on the document, we only have one namespace to retrieve from. However, when looking up from the window, we have 2. We first lookup in the frames collection before falling back to the window collection. This all blows up when you inject the document into the lookup, and you have more than one element or object that collides by name. The following example shows collisions between multiple named items for iframe/embed objects and shows how this collision is avoided by normal function bindings vs markup inline event handlers.


Wrap up

Don't use inline event handlers.

EVER!!!

They are decrepit legacy bindings with some rather non-trivial behavior backing them. And I didn't even get into all of the complexities involved. For instance, since you can't define your own parameter name to accept the event object, we have to create a named parameter called event so you have something to bind to. If we didn't you might be able to get window.event directly, but there is code on the web that just access event. This in turn will break if the element, form or document has a property or named object named event. Wow, how fragile is that!

Figure 1: Chrome showing Lexical Scopes
That said, if you have an interesting use case for when to use one I'd love to hear about it. I'm sure there are some clever uses out there somewhere. For now, this feature feels too much like the with keyword which I hate with a passion. In fact, I think that is how V8 implements these custom scopes, since in their dev tools they display them as With Block's. You can see this in Figure 1.

Sadly these intricate details are not available in the F12 toolbar making them seem like even more magic. I will add a user-voice to this article shortly after posting. I'd like to refer to it, along with the captured image of the Chrome developer tools when describing the behavior I'm after. A chicken and egg problem for sure ;-)

Update: Here is the new link for F12 Support of showing these scope chain items.

No comments:

Post a Comment