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>
Normally, we’d define a new variable using querySelector("#cool")
or getElementById("cool")
to select that element:
var el = querySelector("#cool");
But we actually already have access to #cool
without that rigamorale:
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>
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>
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. This safeguarding technique may very well be the why you — if you’re like me — are learning about this for the first time.
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:
- If the DOM changes, then so does the reference. That makes for some really “brittle” (the spec’s term for it) code where the separation of concerns between HTML and JavaScript might be too much.
- Accidental references are far too easy. A simple typo may very well wind up referencing a named global and give you unexpected results.
- It is implemented differently in browsers. For example, we should be able to access an anchor with an
id
— e.g.<a id="cool">
— but some browsers (namely Safari and Firefox) return aReferenceError
in the console. - It might not return what you think. According to the spec, when there are multiple instances of the same named element in the DOM — say, two instances of
<div class="cool">
— the browser should return anHTMLCollection
with an array of the instances. Firefox, however, only returns the first instance. Then again, the spec says we ought to use one instance of anid
in an element’s tree anyway. But doing so won’t stop a page from working or anything like that. - Maybe there’s a performance cost? I mean, the browser’s gotta make that list of references and maintain it. A couple of folks ran tests in this StackOverflow thread, where named globals were actually more performant in one test and less performant in a more recent test.
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>
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>
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:
- All major browsers automatically create global references to each DOM element with an
id
(or, in some cases, aname
attribute). - Accessing these elements through their global references is unreliable and potentially dangerous. Use
querySelector
orgetElementById
instead. - Since global references are generated automatically, they may have some side effects on your code. That’s a good reason to avoid using the
id
attribute unless you really need it.
At the end of the day, it’s probably a good idea to avoid using named globals in JavaScript. I quoted the spec earlier about how it leads to “brittle” code, but here’s the full text to drive the point home:
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()
ordocument.querySelector()
.
I think the fact that the HTML spec itself recommends to staying away from this feature speaks for itself.
The feature doesn’t seem to work for elements inside shadow DOM.
Super interesting! I actually ran into this a while back, but had no idea what was happening. We had a Script tag that was like:
But, whenever I tried to access
window.config
, I was getting the Script-tag Reference, and NOT the inline-object. I had no idea what was going on, and just ended up removing theid
attribute (since it wasn’t doing anything). Good to know there was at least some logic as to why this was happening.Hey Ben! :D
Weird!
window.config
should have worked in your example. Wondering what could have been.Huh? There’s nothing special about particular tag names, and any browser that that didn’t work in would be violating the spec. Certainly it works just fine in Firefox. I have no idea where this came from.
As a matter of fact, I did find one clear spec violation in this very feature last week (which I haven’t bothered to file at this time): Chromium doesn’t implement it if the document is loaded with XML syntax. So this first URL throws a
ReferenceError
in Chromium (unlike Firefox, or either in the second URL):All up, it’s one of my favourite aggressive minification/golfing tricks (and I use it regularly in one-off scripts for my own use), but it’s not the most robust.
This is completely false.
window["hello-world"]
anditem1
will both work just fine. (Proof: go todata:text/html,<p id=hello-world><p id=item1>
and try it.)This and a few other things in the article suggest that the author hasn’t understood how this whole thing is actually implemented (which is admittedly not awfully clear from the spec if you don’t understand JavaScript’s prototypal inheritance model or the nature of the
window
global object). Without getting into the details: this isn’t the browser setting properties when you provide an element with such-and-such an ID, but rather the fallback, so that when you try accessing a property that doesn’t exist on the global object, it will try finding a suitable object to give you, instead of giving youundefined
straight away.Yeah, that bonus point shouldn’t have slipped in — my bad. Thanks for reporting! I’ll also double-check what you pointed out in the other comment.
No, he’s right, it doesn’t work,try it yourself on chrome bruh
Additional “fun fact”: The same mechanism is available on the HTMLFormElement interface, i.e. on all
<form>
nodes. If you are so unlucky to give any button or input element the IDsubmit
and then try to callform.submit()
you’re in for a really bad surprise:Oh, the sweet inheritance of Microsoft’s IE document.all object model. Sad we didn’t get rid of it when we had the chance..
I had to write a way workaround that like this:
Quite ugly code, I know. That’s the bookmarklet I use to workaround some badly implemented client-side email filters in WordPress comments.
I wonder what was actually happening in your case, because according to the blog post the value of window.config should be the inline-object.
Old time programmers will remember that using IDs as global variables was the dominant way of programming in JavaScript using Internet Explorer. Other browsers refused to implement support for this. The entire industry had to rewrite all JavaScript applications to use getElementById() instead, because this was the standards-compliant way.
But now that we are here, there is no reason to go back to the old way.
I’m struggling to understand “It might not return what you think.”
Shouldn’t it say:
<div id="cool">
rather than “class”? I know you wouldn’t do that but that seems to be way you’re saying?Sadly, this issue/feature exists even for ES6 modules.
This is actually a great feature!
Regarding this example in this article:
One way to fix this would be to remove the
id
attribute from the<img>
element. However, what if you can’t do that? Is there a way to fix the JavaScript to make it resilient to the existance of an element withid
attribute of the valuecookieStore
?That’s cool, but using IDs is controversial on its own. It has the same drawbacks as any other singleton: you think the object is unique and will be forever, then suddenly you face necessity of rewriting your code. I’d better keep querying elements as a collection by class name—the way jQuery works.