DEV Community

naugtur
naugtur

Posted on

The missing link

Here's a riddle I posted

Name the environment in which this code doesn't throw


let w = globalThis
if (!('addEventListener' in globalThis)) throw Error()
while (w) {
    if (Object.hasOwn(w, 'addEventListener')) throw Error()
    w = Object.getPrototypeOf(w)
}
alert('where are we?')
Enter fullscreen mode Exit fullscreen mode

What's in the script

The script first ensures that addEventListener is in globalThis. But in means as own property or anywhere on the prototype. So let's find it on the prototype by iterating down. We throw when found.
Somehow, we run out of truthy prototypes before we find one that has an own property named addEventListener.
Last - as a helpful tip, the code uses alert and it works.

What's the environment

Normally, when you run:

let w = globalThis
while (w) {
  if (Object.hasOwn(w, 'addEventListener')) {
    console.log(`${w} has addEventListener`)
  } else {
    console.log(`${w} does NOT have addEventListener`)
  }
  w = Object.getPrototypeOf(w)
}
Enter fullscreen mode Exit fullscreen mode

the output is:

[object Window] does NOT have addEventListener
[object Window] does NOT have addEventListener
[object WindowProperties] does NOT have addEventListener
[object EventTarget] has addEventListener
[object Object] does NOT have addEventListener
Enter fullscreen mode Exit fullscreen mode

But there's one magical place in the browser where that's not necessarily the case.

When you create an extension, you can define a contentscript that exists to let you interact with the document of a website open in the browser. It's supposed to be isolated from the website's realm though, so that you have your own global context.

The browsers took very different approaches to implementing the isolation. The Firefox implementation is changing the prototype chain of the global and limiting it to window and its prototype, with nothing else visible via code, but with field lookup reaching down the prototype chain further.

The same while loop gets us this:

[object Window] does NOT have addEventListener
[object Window] does NOT have addEventListener
Enter fullscreen mode Exit fullscreen mode

But still,

('addEventListener' in window) === true
typeof window.addEventListener === 'function'
Enter fullscreen mode Exit fullscreen mode

I'm not sure why the behavior is like that, but if I replace globalThis with window, I get a different list of prototypes

[object Window] does NOT have addEventListener
[object Window] does NOT have addEventListener
[object EventTarget] does NOT have addEventListener
[object EventTarget] has addEventListener
[object Object] does NOT have addEventListener
Enter fullscreen mode Exit fullscreen mode

And while I can understand cutting of a bit of prototype as a means of isolation, if it stays on window, that's harder to explain.

Somebody tell me what's going on :)

I'm planning to update this as I figure out more.

Top comments (0)