The only solution I can think of is to use iframes. Each iframe will have it's own newly-created window object, which you can then use in the parent frame (if they're of the same origin).
Math.round = () => null
const frame = document.createElement('iframe')
document.body.appendChild(frame)
Math.round(2.5) // null
frame.contentWindow.Math.round(2.5) // 3
You can even call DOM functions from the iframe against the current page if you supply the right "this" to the function:
// will return your page's body, not the body in the iframe.
// This is because we're telling it to use our document object as "this", instead of the iframe's document.
frame.contentWindow.document.querySelector.call(document, 'body')
In the future, we may get better support for being able to run scripts in their own isolated "realm", where they can have their own separate global enviornment to muck around with. See this upcoming ecmascript proposal to learn more. (at the time of writing, this is currently in stage 2)
Finally, I'll note a couple alternative solutions that may be a better way to go depending on the situation:
- If you have full control over the page, then you should be able to save off global variables before you load these naughty scripts. Alternatively, you could load the script in an iframe to isolate it's bad behavior.
- If possible, it might be better just to let the globals get modified. Scripts really shouldn't be touching them, and if they are, it's probably because they're trying to pollyfill some missing features. Its rare that a script is actually making breaking changes to global function - but if they are, it might be worth it to file a bug report with the script author to fix this behavior.