Detecting variables that are mistakenly or unknowingly added to the global scope can be helpful to debug your apps and avoid naming collisions. The more a web app and its dependencies grow, the more having a good understanding of what’s happening in the global scope becomes important (e.g., to ensure multiple libraries — or even multiple apps! — can coexist on the page without global name collisions).
In this post, I’ll show you how to find what variables have been added into the global scope at runtime in web apps (thanks to @DevelopSean for introducing me to this trick at InVision).
Disclaimer: in this post, I'll reference global scope using the
window
property. Please notice that, in most cases, theglobalThis
property would be a better candidate to access the global object since it works in different JavaScript environments. That said, this post is specific to web (non-worker) contexts, so I think using thewindow
term here makes it easier to follow.
Let's say you want to check what global variables are being added to the window
object on a web page.
As an example, the following (purposely bad) code is adding multiple variables to the global scope (e.g., jQuery
is added by the library itself, i
is added because the script is not using "use scrict"
, etc.).
<html>
<body>
<h1>Hello world!</h1>
<script src="https://unpkg.com/jquery@3.6.0/dist/jquery.js"></script>
<script>
function doSomethingTwice() {
for (i = 0; i <= 2; i++) {
const myString = `hello-world-${i}`;
// Let's imagine we're going to do something with myString here...
}
}
doSomethingTwice();
</script>
</body>
</html>
<html>
<body>
<h1>Hello world!</h1>
<script src="https://unpkg.com/jquery@3.6.0/dist/jquery.js"></script>
<script>
function doSomethingTwice() {
for (i = 0; i <= 2; i++) {
const myString = `hello-world-${i}`;
// Let's imagine we're going to do something with myString here...
}
}
doSomethingTwice();
</script>
</body>
</html>
Typically, you'd probably open the DevTools console and inspect the window
object looking for suspicious variables.
This approach can work, but... it's a lot of work. The browser and the JavaScript engine themselves add a bunch of globals on the window
object (e.g., JavaScript APIs such as localStorage
, etc.), so finding globals introduced by our code is like looking for a needle in a haystack.
One possible way to get around this issue is to grab a list of all the default globals and filter them out from the window
object by running a similar snippet in the DevTools console:
const browserGlobals = ['window', 'self', 'document', 'name', 'location', 'customElements', 'history', 'locationbar', 'menubar', 'personalbar', 'scrollbars', 'statusbar', 'toolbar', 'status', 'closed', 'frames', 'length', 'top', ...];
const runtimeGlobals = Object.keys(window).filter(key => {
const isFromBrowser = browserGlobals.includes(key);
return !isFromBrowser;
});
console.log("Runtime globals", runtimeGlobals)
const browserGlobals = ['window', 'self', 'document', 'name', 'location', 'customElements', 'history', 'locationbar', 'menubar', 'personalbar', 'scrollbars', 'statusbar', 'toolbar', 'status', 'closed', 'frames', 'length', 'top', ...];
const runtimeGlobals = Object.keys(window).filter(key => {
const isFromBrowser = browserGlobals.includes(key);
return !isFromBrowser;
});
console.log("Runtime globals", runtimeGlobals)
Doing so should work, but it leaves two open questions:
- How do you get the
browserGlobals
variables? - Between cross-browser differences and JavaScript API updates, maintaining the
browserGlobals
list can quickly get hairy. Can we make it better?
To answer both questions, we can generate the browserGlobals
list programmatically by populating it with the globals of a pristine window
object.
There are a couple of ways to do it, but to me, the cleanest approach is to:
- Create a disposable iframe pointing it at
about:blank
(to ensure thewindow
object is in a clean state). - Inspect the iframe
window
object and store its global variable names. - Remove the iframe.
(function () {
// Grab browser's default global variables.
const iframe = window.document.createElement("iframe");
iframe.src = "about:blank";
window.document.body.appendChild(iframe);
const browserGlobals = Object.keys(iframe.contentWindow);
window.document.body.removeChild(iframe);
// Get the global variables added at runtime by filtering out the browser's
// default global variables from the current window object.
const runtimeGlobals = Object.keys(window).filter((key) => {
const isFromBrowser = browserGlobals.includes(key);
return !isFromBrowser;
});
console.log("Runtime globals", runtimeGlobals);
})();
(function () {
// Grab browser's default global variables.
const iframe = window.document.createElement("iframe");
iframe.src = "about:blank";
window.document.body.appendChild(iframe);
const browserGlobals = Object.keys(iframe.contentWindow);
window.document.body.removeChild(iframe);
// Get the global variables added at runtime by filtering out the browser's
// default global variables from the current window object.
const runtimeGlobals = Object.keys(window).filter((key) => {
const isFromBrowser = browserGlobals.includes(key);
return !isFromBrowser;
});
console.log("Runtime globals", runtimeGlobals);
})();
Run the snippet above in the console, and you'll finally see a clean list with of the runtime variables 👍
For a more complex version of the script, check this out:
/**
* RuntimeGlobalsChecker
*
* You can use this utility to quickly check what variables have been added (or
* leaked) to the global window object at runtime (by JavaScript code).
* By running this code, the globals checker itself is attached as a singleton
* to the window object as "__runtimeGlobalsChecker__".
* You can check the runtime globals programmatically at any time by invoking
* "window.__runtimeGlobalsChecker__.getRuntimeGlobals()".
*
*/
window.__runtimeGlobalsChecker__ = (function createGlobalsChecker() {
// Globals on the window object set by default by the browser.
// We collect them to then filter them out of from the list of globals (since
// we don't care about them).
// They're populated by "collectBrowserGlobals()" and will contain globals such
// as "location" and "localStorage".
let browserGlobals = [];
// Known globals on the window object that we can safely ignored.
// This list should be populated manually after trial and errors.
const ignoredGlobals = ["__runtimeGlobalsChecker__"];
/**
* Collect the global variables added to the window object by the browser by
* creating a temporary iframe and checking what global variables the browser
* adds on it.
* @returns {string[]} - List of globals added added by the browser
*/
function collectBrowserGlobals() {
const iframe = window.document.createElement("iframe");
iframe.src = "about:blank";
window.document.body.appendChild(iframe);
browserGlobals = Object.keys(iframe.contentWindow);
window.document.body.removeChild(iframe);
return browserGlobals;
}
/**
* Return the list of globals added at runtime (by JavaScript).
* @returns {string[]} - List of globals added at runtime (by JavaScript)
*/
function getRuntimeGlobals() {
// If we haven't collected the browser globals yet, do it now.
if (browserGlobals.length === 0) {
collectBrowserGlobals();
}
// Grab all the globals filtering out variables we don't care about (noise).
const runtimeGlobals = Object.keys(window).filter((key) => {
const isFromBrowser = browserGlobals.includes(key);
const isIgnored = ignoredGlobals.includes(key);
return !isFromBrowser && !isIgnored;
});
return runtimeGlobals;
}
return {
getRuntimeGlobals,
};
})();
/**
* RuntimeGlobalsChecker
*
* You can use this utility to quickly check what variables have been added (or
* leaked) to the global window object at runtime (by JavaScript code).
* By running this code, the globals checker itself is attached as a singleton
* to the window object as "__runtimeGlobalsChecker__".
* You can check the runtime globals programmatically at any time by invoking
* "window.__runtimeGlobalsChecker__.getRuntimeGlobals()".
*
*/
window.__runtimeGlobalsChecker__ = (function createGlobalsChecker() {
// Globals on the window object set by default by the browser.
// We collect them to then filter them out of from the list of globals (since
// we don't care about them).
// They're populated by "collectBrowserGlobals()" and will contain globals such
// as "location" and "localStorage".
let browserGlobals = [];
// Known globals on the window object that we can safely ignored.
// This list should be populated manually after trial and errors.
const ignoredGlobals = ["__runtimeGlobalsChecker__"];
/**
* Collect the global variables added to the window object by the browser by
* creating a temporary iframe and checking what global variables the browser
* adds on it.
* @returns {string[]} - List of globals added added by the browser
*/
function collectBrowserGlobals() {
const iframe = window.document.createElement("iframe");
iframe.src = "about:blank";
window.document.body.appendChild(iframe);
browserGlobals = Object.keys(iframe.contentWindow);
window.document.body.removeChild(iframe);
return browserGlobals;
}
/**
* Return the list of globals added at runtime (by JavaScript).
* @returns {string[]} - List of globals added at runtime (by JavaScript)
*/
function getRuntimeGlobals() {
// If we haven't collected the browser globals yet, do it now.
if (browserGlobals.length === 0) {
collectBrowserGlobals();
}
// Grab all the globals filtering out variables we don't care about (noise).
const runtimeGlobals = Object.keys(window).filter((key) => {
const isFromBrowser = browserGlobals.includes(key);
const isIgnored = ignoredGlobals.includes(key);
return !isFromBrowser && !isIgnored;
});
return runtimeGlobals;
}
return {
getRuntimeGlobals,
};
})();
A couple of final notes:
- This utility can easily run in a Continuous Integration context (e.g., in E2E tests using Cypress) to provide automated feedback.
- I recommend running this utility in browser tabs with no extensions: most browser extensions inject global variables in the
window
object, adding noise to the result (e.g.,__REACT_DEVTOOLS_BROWSER_THEME__
, etc. from the React DevTools extension). - To avoid repeatedly copy/pasting the global checker code in your DevTools console, you can create a JavaScript snippet instead.