Mazzarolo MatteoMazzarolo Matteo

Named element IDs can be referenced as JavaScript globals

By Mazzarolo Matteo


This article has also been published on CSS-Tricks.

Did you know that DOM elements with IDs are accessible in JavaScript as global variables? It’s one of those things that’s been around, like, forever but I’m really digging into it for the first time.

If this is the first time you’re hearing about it, brace yourself! We can see it in action simply by adding an ID to an element in HTML:

<div id="cool"></div>
<div id="cool"></div>

Normally, we’d define a new variable using querySelector("#cool") or getElementById("cool") to select that element:

var el = querySelector("#cool");
var el = querySelector("#cool");

But we actually already have access to #cool without that rigamorale:

<button id="myId" onclick="heyThere()">Click!</button>
 
<script>
  function heyThere() {
    alert(myId.innerHTML); // Prints "Click!"
  }
</script>
<button id="myId" onclick="heyThere()">Click!</button>
 
<script>
  function heyThere() {
    alert(myId.innerHTML); // Prints "Click!"
  }
</script>

So, any id — or name attribute, for that matter — in the HTML can be accessed in JavaScript using window[ELEMENT_ID]. Again, this isn’t exactly “new” but it’s really uncommon to see.

As you may guess, accessing the global scope with named references isn’t the greatest idea. Some folks have come to call this the “global scope polluter.” We’ll get into why that is, but first…

Some context

This approach is outlined in the HTML specification, where it’s described as "named access on the Window object".

Internet Explorer was the first to implement the feature. All other browsers added it as well. Gecko was the only browser at the time to not support it directly in standards mode, opting instead to make it an experimental feature. There was hesitation to implement it at all, but it moved ahead in the name of browser compatibility (Gecko even tried to convince WebKit to move it out of standards mode) and eventually made it to standards mode in Firefox 14.

One thing that might not be well known is that browsers had to put in place a few precautionary measures — with varying degrees of success — to ensure generated globals don’t break the webpage. One such measure is…

Variable shadowing

Probably the most interesting part of this feature is that named element references don’t shadow existing global variables. So, if a DOM element has an id that is already defined as a global, it won’t override the existing one. For example:

<head>
  <script>
    window.foo = "bar";
  </script>
</head>
<body>
  <div id="foo">I won't override window.foo</div>
  <script>
    console.log(window.foo); // Prints "bar"
  </script>
</body>
<head>
  <script>
    window.foo = "bar";
  </script>
</head>
<body>
  <div id="foo">I won't override window.foo</div>
  <script>
    console.log(window.foo); // Prints "bar"
  </script>
</body>

And the opposite is true as well:

<div id="foo">I will be overridden :(</div>
<script>
  window.foo = "bar";
  console.log(window.foo); // Prints "bar"
</script>
<div id="foo">I will be overridden :(</div>
<script>
  window.foo = "bar";
  console.log(window.foo); // Prints "bar"
</script>

This behavior is essential because it nullifies dangerous overrides such as <div id="alert" />, which would otherwise create a conflict by invalidating the alert API.

The case against named globals

Earlier, I said that using global named elements as references might not be the greatest idea. There are lots of reasons for that, which TJ VanToll has covered nicely over at his blog and I will summarize here:

Additional considerations

Let’s say we chuck the criticisms against using named globals and use them anyway. It’s all good. But there are some things you might want to consider as you do.

Polyfills

As edge-case-y as it may sound, these types of global checks are a typical setup requirement for polyfills. Check out the following example where we set a cookie using the new CookieStore API, polyfilling it on browsers that don’t support it yet:

<body>
  <img id="cookieStore"></img>
  <script>
    // Polyfill the CookieStore API if not yet implemented.
    // https://developer.mozilla.org/en-US/docs/Web/API/CookieStore
    if (!window.cookieStore) {
      window.cookieStore = myCookieStorePolyfill;
    }
    cookieStore.set("foo", "bar");
  </script>
</body>
<body>
  <img id="cookieStore"></img>
  <script>
    // Polyfill the CookieStore API if not yet implemented.
    // https://developer.mozilla.org/en-US/docs/Web/API/CookieStore
    if (!window.cookieStore) {
      window.cookieStore = myCookieStorePolyfill;
    }
    cookieStore.set("foo", "bar");
  </script>
</body>

This code works perfectly fine in Chrome, but throws the following error in Safari.:

TypeError: cookieStore.set is not a function

Safari lacks support for the CookieStore API as of this writing. As a result, the polyfill is not applied because the img element ID creates a global variable that clashes with the cookieStore global.

JavaScript API updates

We can flip the situation and find yet another issue where updates to the browser’s JavaScript engine can break a named element’s global references.

For example:

<body>
  <input id="BarcodeDetector"></input>
  <script>
    window.BarcodeDetector.focus();
  </script>
</body>
<body>
  <input id="BarcodeDetector"></input>
  <script>
    window.BarcodeDetector.focus();
  </script>
</body>

That script grabs a reference to the input element and invokes focus() on it. It works correctly. Still, we don’t know how long it will continue to work.

You see, the global variable we’re using to reference the input element will stop working as soon as browsers start supporting the BarcodeDetector API. At that point, the window.BarcodeDetector global will no longer be a reference to the input element and .focus() will throw a "window.BarcodeDetector.focus is not a function" error.

Conclusion

Let’s sum up how we got here:

As a general rule, relying on this will lead to brittle code. Which IDs end up mapping to this API can vary over time, as new features are added to the web platform, for example. Instead of this, use document.getElementById() or document.querySelector().

I think the fact that the HTML spec itself recommends to staying away from this feature speaks for itself.