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?')
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)
}
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
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
But still,
('addEventListener' in window) === true
typeof window.addEventListener === 'function'
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
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)