window.clusterFuck

Posted 4 years and 7 months ago

I started a project called Terra a few weeks ago. I added a conditional to init() that would complain if I’d already defined the library:

;(function ( exports ) {
  function init () {
    if ( !exports.terra ) {
      // do initialization things
    } else {
      throw "You've already defined terra. Pay attention..";
    }
  }

  // more stuff happens...

  init();
}) ( this );

I’ll admit that this is a really ugly pattern for handling namespaces, but even common patterns are affected by this. Let me take you on a journey through the biggest browser wat I’ve ever seen.

I ran the code and the console yelled at me.

"You've already defined terra. Pay attention.."

Strange… but maybe I forgot a var somewhere. I set some breakpoints in DevTools and stepped through the execution.

  1. The main IIFE is called
  2. Some local variables are defined
  3. init() is called.. wait, did I miss something?
  4. window.terra is already defined????!!?!!!?!?!?!?!
  5. Error is thrown, I get confused.

The only scripts on the page were ones that I’d written, and I definitely hadn’t defined terra. I switched to a new branch and deleted almost everything.

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Terra</title>
</head>
<body>
  <div id="terra"></div>
  <script>
    ;(function ( exports ) {
      function init () {
        if ( !exports.terra ) {
          console.log( 'Everything is awesome.' );
        } else {
          console.log( 'The night is dark and full of terrors.');
        }
      }

      init();
    }) ( this );
  </script>
</body>
</html>

Do you see it? I didn’t yet, and was hella confused when the console returned:

The night is dark and full of terrors.

So I made it simpler:

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Terra</title>
</head>
<body>
  <div id="terra"></div>
  <script>
    console.log( !window.terra );
  </script>
</body>
</html>

…and got:

False

Either someone had added a global property called terra to the latest build of Chrome, or it was adding that <div id="terra"> to the global namespace. But… that would be insane. Browsers wouldn’t add element IDs to window, would they?

They would.

I tried Firefox. Defined. I tried Safari. Defined. I tried IE. Super-duper defined. The room was spinning; I had read the scriptures of Crockford and Resig and this was blasphemy. How many times had I inadvertently defined window.test, window.baz, window.boobies

Digging deeper

Unsurprisingly, it seems that Internet Explorer can be thanked for this awesome “feature”. Other browsers felt compelled to follow suit, and soon enough the behaviour made it into the official HTML spec.

The Window interface supports named properties. The supported property names at any moment consist of the following, in tree order, ignoring later duplicates:

  • the browsing context name of any child browsing context of the active document whose name is not the empty string,
  • the value of the name content attribute for all a, applet, area, embed, form, frameset, img, and object elements in the active document that have a non-empty name content attribute, and
  • the value of the id content attribute of any HTML element in the active document with a non-empty id content attribute.

So, there you have it. Any elements with an id or name attribute will have a reference in the global namespace.

What does it all mean?

Firstly, this does not make it okay for you to start interacting with the DOM using these variables. Cross-browser implementations are shaky, and hopefully at some point this will fade out of the spec. On top of that, any newly defined globals will overwrite these references. Stick with document.getElementById() and document.getElementsByName(). Or $(), if you’re into that sort of thing.

These element references likely won’t cause you any trouble, especially if you’re being a good neighbor in the global namespace. If you’re using browserify (which you should be), RequireJS, or any strict module system, just pick specific global variable names and kept them to a minimum.

Weird stuff can happen if there’s a collision; let’s take a look at the 5 most common namespace-checking patterns for object literals:

/*
Option 1: var myApplication = myApplication || {};
Option 2  if(!MyApplication) MyApplication = {};
Option 3: var myApplication = myApplication = myApplication || {}
Option 4: myApplication || (myApplication = {});
Option 5: var myApplication = myApplication === undefined ? {} : myApplication;
*/

Options 1, 3, and 5 shadow myApplication with the initial var declaration, so you’ll end up with myApplication equal to {}. Option 2 and 4 though? Not so lucky. Since there’s no var shadowing the variable, these patterns leave you with a DOM element sitting in place of your fresh new object. As soon as you try to iterate over the object’s properties or run one of its methods, you’re working with a different object than the one you expected.

Here a few more ways that these new variables can get you in trouble (assuming here that we’ve defined an element with name="ticker"):

Checking for existence

As we saw earlier, conditional code such as

if ( !window.ticker ) {
  window.ticker = 0;
}
window.ticker++;

Can cause all sorts of errors and heartbreak down the road. Here’s a safer way to check!

Misspelling

All of a sudden,

var picker = {};
picker.item1 = 'Corn Flakes';
ticker.item2 = 'Buttons';
picker.item3 = 'Hair';

Doesn’t throw a ReferenceError and the property we meant to interact with isn’t touched.

Blocking external libraries

Sadly, you don’t have much control over this one. Other developers who aren’t as enlightened as me and you may write their libraries in such a way that named element references can block their initialization. A good way to avoid this is to

  1. avoid using ids with the same name as libraries you’re using (duh), and
  2. post an issue to the library’s repository mentioning that their library isn’t safe for loose-cannon-element-namers.

Conclusion

Though the globalization of element ids and names really surprised me, the fact that it gets such little attention means that it’s probably not a huge deal. Regardless, adding all of these variables to the global namespace has no real advantage, and allows developers to make mistakes that are really annoying to debug. Apart from maintaining legacy code written by the few people who use this rule, I can’t see any reason to keep the behaviour in modern browsers.

For now just keep smiling, and let’s all hope that someday soon we’ll be rid of them.

Addendums

2014-03-12

Redditor minrice2099 came up with a clever way to check if a variable comes from a named element. Checking x in Object.keys( window ) will return True if x is defined explicitly (with var or window.) or is inherited through the prototype chain; otherwise, it will return False. This is different than checking .hasOwnProperty because it will respect properties found up the prototype chain.

From here, we can check existence using:

if ( window.x in Object.keys( window ) ) {
  // do as you must...
}

This works in all modern browsers, but use at your own risk if you’re looking to support past / future browsers.

comments powered by Disqus