Hacker News new | past | comments | ask | show | jobs | submit login
Named element IDs can be referenced as JavaScript globals (css-tricks.com)
177 points by mmazzarolo on Sept 27, 2022 | hide | past | favorite | 109 comments



The global scope polluter has pretty bad performance and interop surprises, you shouldn't depend on it and instead use getElementById even if it's a bit more verbose.

It uses a property interceptor which is fairly slow in v8:

https://source.chromium.org/chromium/chromium/src/+/main:out...

to call this mess of security checks:

https://source.chromium.org/chromium/chromium/src/+/main:thi...

which has this interop surprise:

https://source.chromium.org/chromium/chromium/src/+/main:thi...

which in the end scans the document one element at a time looking for a match here:

https://source.chromium.org/chromium/chromium/src/+/main:thi...

In contrast getElementById is just a HashMap lookup, only does scanning if there's duplicates for that id, and never surprisingly returns a list!


I really wish in the source code it was actually named globalScopePolluter()


Is there a reason to not use querySelector, since it’s a lot more flexible? One reason jQuery became so popular is because the DOM was painful to use. Things like querySelector fix that.


> Is there a reason to not use querySelector

getElement is slightly faster, but not by enough to care IIRC so I use querySelector for consistency and it's flexibility.

> One reason jQuery became so popular is because the DOM was painful

I would say that is the key reason, with everything else being collateral benefits. Assuming you combine element selection, dealing with legacy incompatibilities, and function chaining to reduce boilerplate code, under the same banner of "making the DOM less painful".


...and portability.


Between browsers?

I was counting that in "dealing with legacy incompatibilities".


QuerySelectors are slow. Epic slow. I would also argue that querySelectors are far less flexible and became popular because they are instead easy.

https://jsbench.github.io/#b39045cacae8d8c4a3ec044e538533dc

ProTip: Without numbers performance opinions are wrong by several orders of magnitude 80% of the time.


I’m getting 70mops byId and 23mops qs-#id. This looks like making a huge difference until I add these cases:

  3: "abc”.replace("a", "b")
  4: "1" * 2
Which result in 15mops and 74mops respectively. This test measures diameters of neutrinos so to say.


My experience is that most developers tend to guess at performance and throw away numbers they disagree with. As a result performance testing is only something product owners care about.


BTW, in my experience getElementById() is still fastest.


In isolation definitely, but in real world code it might be faster to use querySelector for branchy code if it doesn’t always use an id. As with everything, if it’s not performance-sensitive write the code that’s easier for humans to read, and if it is measure first.


I'm not sure what you're trying to say here, as it's tautologically correct that getElementById can't be used in cases where you want to select on more than just the id. Do you mean a use case where you have branchy code that produces a selector string that has some id only paths?


Yes. Branchy code which could sometimes use getElementById and other times use querySelector may be faster if it always uses querySelector, even if that call itself is slower. The reason for this is that the JITs sometimes deoptimize on branchy logic with inconsistent property access between branches. They also deoptimize on branchy logic defining intermediate values, but much less often when the value is a consistent type like a string (selector).


This would only be relevant if you're doing something like

    var theFunction = condition ? "querySelector" : "getElementById";
    ...
    document[theFunction](...)
it won't apply to

    if (condition)
      document.querySelector(...)
    else 
      document.getElementById(...)
As from the point of view of the runtime the latter has two call sites, and each one is monomorphic and will very quickly (first layer of the JIT tower generally) become a Structure/Shape/HiddenClass check on `document` followed by a direct call to the host environment's implementation function (or more likely the argument checking and marshaling function before the actual internal implementation).

It is possible that the higher level JITs pay attention to the branch counts on conditions or use other side channels for the deopt, but for host functions it's generally not something that will happen as the JITs see natively implemented functions as largely opaque barriers - they only have a few internal (to the runtime itself) cases where they make any assumptions about the behaviour of host functions.


> As from the point of view of the runtime the latter has two call sites, and each one is monomorphic

I expected that to be the case but I’ve actually measured it and it’s not always. It is, when the object being accessed has a consistent shape/hidden class, as you mention, but a lot of times they don’t. A weird case is native interfaces because while the host functions are opaque and you’d expect they have a stable shape the interfaces themselves are often mutable either for historical reasons or shortcuts taken in newer proposals/implementations. Accessing document.foo isn’t and can’t be monomorphic in many cases, even if it can be treated that way speculatively. But branchy code can throw out all sorts of speculation of that sort. I don’t know which level of the JIT this occurs at, I’m just speaking from having measured it as a user of the APIs.


Hmmm, how did you measure?

This isn't me disagreeing, just me being surprised and trying to think of why the optimizer falls off.

JSC at least has flags on the structure that track which ones will bollocks up caching (e.g. the misery that is looking up things in the prototype chain if the object in question has magic properties that don't influence the structure).

One thought I have is if your test case was something like

   if (a)
     obj.doTheNativeThing()
   else
     obj.doTheOtherNativeThing()
(or whatever)

and you primed the caches by having a being true/false be a 50/50 split, vs all one way. My thinking (I have not done any of the debugging or logging) is that the branch that isn't taken won't insert any information about the call target. I can see that resulting in the generated code in the optimizing layers of the JITs being something along the lines of

    if (a)
       call _actualNativeFunction
    else
       deopt
The deopt terminates the execution flow so then in principle the VM gets to make assumptions about the code state after the whole if/else block, but more importantly the actual size of the code for the function is smaller, and so if you were close to the inlining limit dropping the content of the else branch _could_ result in your test function getting inlined, and then follow on optimizations can happen in the context of the function that you use to run your test with. Even if there aren't magic follow on optimizations removing the intermediate call can itself be a significant perf win.

Testing the performance of engines was super annoying back when I worked on JSC, as you have to try and construct real test cases, but that means competing with your test functions being inlined. JSC (and presumably other engines) have things you can do (outside of the browser context) to explicitly prevent inlining of a function, but then that is also not necessarily realistic. But it's super easy to accidentally make useless test cases, e.g.

    function runTest(f) {
      let start = new Date;
      for (let j = 0; j < 10000; j++)
        f()
      let end = new Date;
      console.log(end - start)
    }

    function test1() {
      ...
    }

    function test2() {
      ...
    }

    runTest(test1)
    runTest(test2)
In the first run with test1, f (in runTest) is obviously monomorphic, so the JIT happily inlines it (for the sake of the example assume both functions are below the max inlining size). The next run with test2 makes f polymorphic so runTest gets recompiled and doesn't inline. Now if test1 and test2 are both small the overhead of the call can dominate the cpu time taken which means that if you simply force no inlining of the function you may no longer be getting any useful information, which is obviously annoying :D


The performance difference is negligible. Both methods can return 70k-100k selections in 10ms.


On what hardware?

Also there's a performance cliff when you have a lot of unique ids (or selectors in use from JS).

When you hit the cache querySelector is primarily a getElementById call and then some overhead to match the selector a second time (which chrome should really optimize):

https://source.chromium.org/chromium/chromium/src/+/main:thi...

But if you have more than 256 selectors and ids in use:

https://source.chromium.org/chromium/chromium/src/+/main:thi...

You'll start to hit the selector parser a lot more and then querySelector will be a fair bit slower going through the CSS parser.


I had projects where this contributed to a visibly perceivable difference. This may have involved SVG, though.


Oh I can definitely see that coming up with SVG or deep extensive XML trees yeah.


That’s surprising, webkit+blink and I’m guessing gecko all optimize the query selector cases. I assume it’s the cost of the NodeList (because NodeLists are live :-/)


May be worth testing it against getElementsByClassName(), which also returns a live collection.


I actually just went and tested and in webkit at least my 100% perfect test case I had querySelector taking 2x longer than getElementById. I tried understanding what the current webkit code does but the selector matching code is now excitingly complex due to the CSS JIT.

Many many years ago I recall querySelector starting out with a check for #someCSSIdentifier and shortcutting to the getElementById path, but maybe my memory is playing tricks on me.


Yup that's what it did, and Chrome still does. After much research and prototyping the CSS JIT didn't improve real world content (especially given the complexity) so it was never added to Chrome.


I'm surprised to find that this trick still works even in the new backwards-incompatible JavaScript Modules (using <script type="module">), which enables "strict" mode and a number of other strictness improvements by default.

I believe it works because the global object ("globalThis") is the Window in either case; this is why JavaScript Modules can refer to "window" in the global scope without explicitly importing it.

    <!DOCTYPE html><body>
        <div id="cool">cool</div>
        <script>
            console.log(this); // Window
            console.log(globalThis); // Window
            console.log("script", cool.innerHTML); // script cool
        </script>
        <script type="module">
            console.log(this); // undefined
            console.log(globalThis); // Window
            console.log("module", cool.innerHTML); // module cool
        </script>
    </body></html>
This seems like a missed opportunity. JavaScript Modules should have been required to "import {window} from 'dom'" or something, clearing out its global namespace.


There is some effort to standardize something along these lines. Well, some things which combined would achieve this. It’s too late to bake it into ESM, but I believe it’ll be possible with ShadowRealms[1] and/or SES[2], and Built-in Modules (JS STL)[3].

1: https://github.com/tc39/proposal-shadowrealm

2: https://github.com/tc39/proposal-ses

3: https://github.com/tc39/proposal-built-in-modules


Luckily the Reddit thread has the Yu-Gi-Oh! jokes so I don't have to repeat them here.


I don’t know what jokes or Reddit thread you’re referring to or why it has anything to do with my comment referencing three technical proposals, but I’ll take your word for it that you don’t have to repeat them here.


https://www.reddit.com/r/javascript/comments/t8mdli/future_j... if you’re curious. Didn’t mean to be overly vague, I just think ShadowRealms is an unusually funny name for a technical proposal. I guess it doesn’t have the same effect if you didn’t grow up watching the same cartoons.


I don't think this article is complete. It mentions no pollution, which is true of window and most HTML elements, but not always. Check this out, you can set an img name to getElementById and now document.getElementById is the image element!

Here's a minimal example (https://jsfiddle.net/wc5dn9x2/):

    <img id="asdf" name="getElementById" />
    <script>
        // The img object
        console.log(document.getElementById);

        // TypeError: document.getElementById is not a function :D
        console.log(document.getElementById('asdf'));
    </script>
I tried poking around for security vulnerabilities with this but couldn't find any :(

It seems that the names overwrite properties on document with themselves only for these elements: embed form iframe img object

Edit: Here's how I found this: https://jsfiddle.net/wc5dn9x2/1/


Note that this is with the name attribute, not the id attribute the article is discussing.


Good catch. That would explain why it wasn't mentioned then


Curiously the article doesn't mentions it, but theses kinds of vulnerabilities are named DOM clobbering if you want to know more about it!

It's weirdly not that discussed on the web, most probably because it require a pretty specific situation.


Thank you for this! I had a feeling it wasn't a security issue. I closed my ticket saying it might be one due to finding websites mentioning Dom clobbering


Another similar gotcha is that the global-scoped `name` variable must be a string. See https://developer.mozilla.org/en-US/docs/Web/API/Window/name for details.

    var name = true;
    typeof name; // "string", not "boolean"
Luckily, this is not true within ES modules which you probably use most of the time anymway.


That's not magic, it's just how property getter and setters work on the global:

    <script>
        var _value = "test value";
        Object.defineProperty(window, "testName", {
            get: () => _value,
            set: (value) => { _value = String(value) },
        });
    </script>
    <script>
        var testName = {};
        // prints [object Object] string
        console.log(testName, typeof testName);
        var name = {};
        // prints [object Object] string
        console.log(name, typeof name);
    </script>
the `var` doesn't create a new property since the getter and setter already exist.

Other properties have the same behavior, for example `status`.

Note: there's also LegacyUnforgeable which has similar behavior: https://webidl.spec.whatwg.org/#LegacyUnforgeable

Even if you're not using modules, using an IIFE avoids all this by making your variables local instead of having them define/update properties on the global.


It takes a special kind of human to name variable "name" but not have it be a string.


Something like

  name = {first: "Jane", last: "Doe"}
isn't obviously unreasonable. Which actually sets name to the string "[object Object]".


Falsehoods programming languages believe about names.


I work with such humans! I was looking at that exact situation a few moments ago.


I can imagine someone doing this if they were using "name" as a verb


I've been doing JS for like fifteen years, this one I never knew. Wow.

I must have never used "name" as a name for a global variable or just for ones that were strings.


This is one of those things that pops up every year or two years. Unfortunately, the person writing about the new discovered weird trick almost always fails to precede the article with a big, red, bold "Please don't ever do this".


and then someone always follows up with "Please don't ever do this", without explaining WHY you should never do this:

https://wikipedia.org/wiki/Wikipedia:Chesterton's_fence


The article already explains that thoroughly.


Nice article, thanks for sharing it.


It's has been explained enough times. It's just that looking things up for yourself seems to have gone out of fashion.


That doesn’t help people who stumble upon this when searching for the problem. All the “look it up” response does is make sure the search results are a bunch of content saying “look it up”, which isn’t really that helpful.


That's a classic.

Get my hopes up finding an old forum post asking my question, hoping to find answers. All the answers are "use Google/etc", which is how I got there.


It is explained fairly early in the article.

This used to be done quite a lot in the early JS days when scope was kind of thrown out the window (no pun) and you just did whatever dirty thing you needed to in order to make a page work.


lol, I just searched "problem with referencing named element ids as javascript globals": the first result is the linked article and the second result is, you guessed it, this thread with your comment on top.


>the person writing about the new discovered weird trick almost always fails to precede the article with a big, red, bold "Please don't ever do this"

> It's has been explained enough times. It's just that looking things up for yourself seems to have gone out of fashion.

It appears you've countered your own complaint.


I discovered that with a couple of friends while in JavaScript class. Every one of us was like "this is actually horrible".


This has been a thing since the 90s. I really wish we'd done away with it for any document that specifies itself as HTML5.

It's great for hacking a tiny script together, however.


Yeah, HTML5 explicitly documented the compatible behaviors between browsers to reach uniformity, which meant standardizing a lot of weird stuff instead of trying to fix it.

See for example this thread where Mozilla tried to not do this: https://bugzilla.mozilla.org/show_bug.cgi?id=622491


Yep, same here. The only time I use this bit of knowledge nowadays is in the console. If I see a tag has an ID, I save myself a few characters by just referring to it as a variable since I know it's already there anyways.

IDs were the only way to get a reference to an element early on if I'm remembering correctly. Or maybe the DOM API just wasn't well known. All the examples and docs just used IDs, that I can remember for sure.


This always reminded me of PHP’s infamous register_globals. For those unfamiliar, anything in the ‘$_REQUEST’ array (which itself comprises of $_POST, $_GET, and $_COOKIE merged together) is added to the global scope. So if you made a request to index.php?username=root, $username would contain “root” unless it was explicitly initialized it before it was used.


iirc this doesn't work in Firefox? or at least it doesn't work the same way as in Chrome. I developed a tiny home-cooked app[0] that depended on this behavior using desktop Chrome which then broke when I tried to use it on mobile Firefox. I then switched it to using

  document.getElementById
like I should have and everything worked fine. Like others in this thread, I recommend not relying on this behavior.

[0]: https://www.robinsloan.com/notes/home-cooked-app/


> It is implemented differently in browsers

In 2022, that alone is enough to wipe it from my toolbox as a web developer. Ain't nobody got time for that.

(... there are lots of other reasons it'd be bad practice to rely on this as well, although it's nice for debugging when available).


Seems like something that could have been made safer just by name spacing it a bit better.

Something like “window.elements.myDiv”? I wonder why the decision to go straight to the root.


`document.all` can be used in this way:

  <div id="foo"></div>
  <script>
    const { foo } = document.all
    // do something with foo
  </script>
Don't use it though, it's deprecated as well[1].

[1]: https://developer.mozilla.org/en-US/docs/Web/API/Document/al...


You can make this yourself with Proxy. I get a lot of mileage out of this:

    // proxy to simplify loading and caching of getElementById calls
    const $id = new Proxy({}, {
        // get element from cache, or from DOM
        get: (tgt, k, r) => (tgt[k] || ((r = document.getElementById(k)) && (tgt[k] = r))),
        
        // prevent overwriting
        set: () => $throw(`Attempt to overwrite id cache key!`)
    });

Now if you have

    <div id="something></div>
You can just do

    $id.something.innerHTML = 'inside!';


The Netscape of the 90s wasn't interested in making features ‘safe’. They were about throwing out features as quickly as possible to see what would stick.

The simplest possible syntax is to make named elements available globally, and if that clashes with future additions to the DOM API then well that's a problem for some future idiots to worry about.

as a strategy it worked pretty well, unfortunately


As the article points out, this initiative was an 90s IE one and the Gecko team (Firefox, post-Netscape) were against it.


I saw this "shortcut" used in code snippets, on online JS/CSS/HTML editors like JSFiddle. It did not even occur to me this was part of JS spec, I thought the editor was generating code behind my back!


> It did not even occur to me this was part of JS spec,

It has nothing to do with JS spec; it's part of the DOM as defined by the HTML spec.


Now I'm worried of using IDs and finding issues with globals in JavaScript. Seems to be a curious issue to be debugged.


Avoid globals at all costs - use IIFE [1] instead, wrapping your function in parenthesis and invoking it right away.

[1] https://developer.mozilla.org/en-US/docs/Glossary/IIFE


If you have access to `let`, you can just put `let` declarations into a block. No need for a function to establish scope.


When, today, does it make more sense to organize things around IIFEs and not ES6 modules?


It's 2022, you can use ES2015 modules now. We can leave IIFE to the dustbin of the past.


And then get coworkers to remove it because they don’t understand that you can create scopes like that


    {
        let foo = 1
    };
    // foo is undefined here


If you read the article and the spec, you'll see that any explicitly created variables will always take precedence over automatic IDs, so any globals will always override these IDs.


In the additional considerations section [1], they mention about not consistent behaviors between browsers. Those are the kind of issues that are quite difficult to debug.

[1] https://css-tricks.com/named-element-ids-can-be-referenced-a...


And that could be the problem if you try to access an element by id but a variable has the same name. This renders this option pretty useless.


You shouldn't be using IDs anyways. They are just bad for a lot of reasons. You can only have one on a page and it reduces your reusability. Use classes instead.


ID's aren't bad, they're unique identifiers, and useful for (deep) linking to specific pieces of content within documents. Please use ID's as liberally as you please, and use them for their proper use.


Use ids when JS needs to reference unique elements. Use classes for styling and accessing groups.


JS can do just as well with unique classnames, which avoids issues with ids like those given in the article.


I always presumed this would usually entail a performance hit, since you are accessing something that is not defined as unique.


It's an infinitesimal performance decrease, and it avoids a lot of other issues.


I discovered this the hard way and I am still really torn. The entire window global object is just a minefield.


>To add insult to the injury, named elements are accessible as global variables only if the names contain nothing but letter.

This doesn't seem to be true as shown within this fiddle: https://jsfiddle.net/L785cpdo/1/

Bear in mind that only undefined elements will be declared this way


Author here. That was a mistake on my part, it shouldn't have slipped in :) I removed that section, thanks for reporting!


For me, the disadvantage above any listed on the blog is that if I saw this global variable referenced in some code (especially old code, where some parts might be defunct), I would have absolutely no idea where it came from, and I bet a lot of others would struggle too.


Isn't the opposite of

>So, if a DOM element has an id that is already defined as a global, it won’t override the existing one.

So, if a global has name of the id of a DOM element, it won’t override the existing one?

Wouldn't it be clearer to say globals always before DOM ids?


This was mostly useful back in the days when we had to manually query dom during development and debugging. I've seen some pretty horrible things but never have I seen this in a codebase, not even in a commit


I remember using it on the first Javascript I ever used around 20 years ago. I naively assumed that the DOM was like state in a more procedural language and this variable trick played into that.


I've definitely used this shortcut to make CodePens less verbose…

I wouldn't use it in production, but it's handy for banging together a proof-of-concept.


It hurts


I think it would hurt less with TypeScript global types. Just need to know what IDs you'd expect to find in the DOM.


the ID's in DOM will never conflict or cause an issue with your own JS code. You can't reliably use 'named access on the window object' (the name of this feature) because of this, so it's never a problem, and also largely useless.


It would make sense to disable this with new release of HTML, for example if the author uses an HTML6 doctype.


And named form controls are accessible as properties of the same name on their form element.


*rigamorale

Should read “rigamarole”


I've always thought it was 'rigmarole'!

Today I learned, it's both!

https://en.wiktionary.org/wiki/rigmarole


*rigmarole, if we're being pedantic, but I suspect the contemporary spelling "rigamarole" is gaining on the proper spelling, and that's one of the wonderful/terrible things about the English language.


This is my favorite HTML and JS feature!


I don't want to sound like I have an axe to grind (but I do), but this is the kind of feature/wart that shows the age of the HTML/CSS/JS stack.

The whole thing is ripe for a redo. I know they get a lot of hate, but of all the big players in this space I think FB is the best equipped to do this in a way that doesn't ruin everything. I just wonder if they have an incentive (maybe trying to break the Google/MS hegemony on search?).


The best equipped to do this are Google/MS/Apple because they actually control the source code of relevant contemporary browsers.


I think that this is the case (right now) because of Apple's stranglehold on the browser on iOS and the complex relationship between Google/Apple.

If FB could launch a browser on iOS that was in their walled garden, not only would it quickly receive wide adoption but it might become people's primary browser.

Not that I necessarily think that's a good thing, mind you.


Why would it quickly receive any adoption? Of all of the behemoths, I would trust FB the least here. Not that I trust any of the other big players enough not to use Firefox everywhere I can.


The word "trust" doesn't factor into ~90% of users' decisions.

If FB says "hey install this app," they will install it.


> The whole thing is ripe for a redo

Web developers have worked around quirks for as long as I can remember. The stack has many warts, but we learn to adapt to them. Like 90% of a web developer's job is working around gotchas, and will continue that way. A 'redo' might not be needed. Developers need something to moan about and need something to keep them employed :)


I find it pretty funny that we humans have invented all these transpilers and bundlers, invested probably billions of dollars in JITs, just to keep writing JS


Could you explain how rewriting one of the worlds most complex and critical specifications would break of the Google/MS hegemony on search?


Sorry, what I meant was:

"If FB decided to try and break into search, then they might decide to attack the HTML/CSS/JS stack."

Not the other way around.


There’s always WASM, and I think Zuck is more interested in VR than trying to push a new web standard.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: