The problem: global read-only properties
First, note that this problem exists in a Web environment, i.e. in a browser.
Other JS environments might not have this problem.
var closed = 0;
in the global scope doesn’t create a binding that refers to a number and remains being the boolean false
.
The reason for this is that closed
refers to window.closed
in this circumstance; it’s always a boolean, and redefining it produces a linter warning like “Redefinition of closed
”.
This read-only property indicates whether the referenced window is closed or not.
Variable names that behave like this can be found in these lists:
There also used to be documentation on MDN about a mixin called WindowOrWorkerGlobalScope
, whose properties would fit into this list; however, this just refers to things that are both in Window
and in WorkerGlobalScope
.
Tiny piece of context: MDN did a refactoring of their documentation where they removed all these “mixins” in favor of standard interface names.
You can find archived versions of this list.
Window
or WorkerGlobalScope
are two of the interfaces that globalThis
can be an instance of in Web environments.
Run the snippet to get a full list of variable names that you can’t safely use in the global scope:
const props = Object.entries(Object.getOwnPropertyDescriptors(globalThis)),
undesirable = {
set: (desc) => desc,
configurable: (desc) => !desc,
writable: (desc) => !desc
};
Array.from(document.querySelectorAll("[id]"))
.forEach((span) => span.innerHTML = props
.filter(([prop, {[span.id]: desc}]) => undesirable[span.id](desc))
.map(([prop]) => `<code>${prop}</code>`)
.join(", "))
code{
background: #eee;
padding: 1px 3px;
}
<p>Properties that have a setter which may change the type or invoke some function, when a value is set to it:</p>
<span id="set"></span>
<hr/>
<p>Properties that are not configurable:</p>
<span id="configurable"></span>
<hr/>
<p>Properties that are read-only:</p>
<span id="writable"></span>
You’ll notice, it’s a lot.
It’s also a lot of short, common variable names like name
, length
[1], [2], status
[1], [2], self
, top
, menubar
, and parent
.
Furthermore, regarding the invocation of some function when assigning to a setter, something like var location = "Podunk, USA";
actually redirects you to the location ./Podunk, USA
.
There’s a related issue of using function names like lang
, checked
, autocomplete
, evaluate
, or animate
in event attributes like onclick
: there, not only the window
properties are included in the scope chain, but also all scopable properties of the entire prototype chain of the current HTMLDocument
and of the current specific Element
(e.g. using <a onclick=""></a>
provides access to everything starting at HTMLAnchorElement.prototype
) and perhaps some more, e.g. anything from the form
property (of form elements) if it exists.
All this is also explained in this answer to a related question.
This problem is also exhibited when relying on DOM nodes with an id
to become global properties.
Solutions
There is a handful of solutions to this.
I’ll list them in a subjective order of best to worst.
Use modules instead
Running your JavaScript code as modules instead of scripts not only is the new cool thing to do, it also just doesn’t have this problem.
index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Your site</site>
<script type="module" src="main.mjs"></script>
</head>
<body>
<!-- Content. -->
</body>
</html>
main.mjs
:
const top = { spinning: true },
window = { type: "clear", frame: "wood" };
let document = { type: "LaTeX", content: "\\documentclass[a4]{…} …" },
closed = 0;
var location = "Podunk, USA";
// All of these are the variables defined above!
console.log({ top, window, document, closed, location });
Modules also come with a few other helpful features like implicit deferment in relation to DOM parsing.
Wrap everything in a new scope
In a script’s global scope, you will encounter this problem.
In a new function scope, you don’t have this problem:
(function(){
const closed = 0; // Or `let` or `var`.
$(function(){
console.log(closed);
});
})();
You can also create a more useful scope, for example jQuery’s $(function(){
…});
which waits for the DOM to be loaded.
This, by the way, is probably the option with the highest compatibility while still being one of the better options.
Since I don’t use jQuery, I prefer to wrap everything in a DOMContentLoaded
listener where I can scope all the variables I need, and, in addition, use "use strict";
:
addEventListener("DOMContentLoaded", () => { "use strict";
// Code goes here.
});
This avoids the clash between global properties and global variables.
Always use const
. If you can’t use const
, use let
. If you can’t use let
, use var
.
… But you’ll never need var
!1
Using const
or let
in scripts, as Ankit Agarwal’s answer suggests, mitigates this problem:
const closed = 0;
console.log(closed); // 0
However, this only works for most variable names, but not all — const closed = 123; let history = "Hello, world!";
all work, but const window = 123;
, const top = 123;
or let document;
don’t.
1: Although there may be one edge-case.
And obviously, older browsers still need var
.
Just use different variable names
Sounds obvious, but this isn’t very robust.
New global properties may always pop up, so global variables may clash with them at any time.
Since this would obviously break several scripts all over the Internet, global properties which are added nowadays don’t really create much of an issue.
const history = "Regency Era";
works just fine and makes history
usable as a variable that refers to this string.
var history =
…;
still doesn’t, and it throws an error in strict mode, and fails silently in loose mode.
This has historically led to compatibility issues which is why this isn’t a robust option, especially when using var
, which in turn is another strong argument against var
.
Reassigning older properties, such as document
, will fail in scripts, in certain environments, but every variable name will work in a module, in every environment, which is why this is the top recommended option.
closed
refers towindow.closed
in global scope. Don’t define variables globally. Wrap everything in an IIFE if you must. – Stadtholder