Created
March 11, 2019 10:48
-
-
Save CertainPerformance/68d3f2f675b477b3b762a17adfb6b579 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// For reference: | |
// https://stackoverflow.com/questions/54776759/how-to-avoid-accidentally-implicitly-referring-to-properties-on-the-global-objec | |
const fakeWindow = (() => { | |
// Declare Object and ReferenceError as variable names in this scope | |
// since implicit references to them on window will eventually fail during the below loop | |
const { Object, ReferenceError } = window; | |
const fakeWindow = Object.create(Object.getPrototypeOf(window)); | |
const descriptors = Object.getOwnPropertyNames(window) | |
.reduce((a, propName) => { | |
a[propName] = Object.getOwnPropertyDescriptor(window, propName); | |
return a; | |
}, {}); | |
let current = window; | |
while (current !== null) { | |
Object.getOwnPropertyNames(current).forEach((propName) => { | |
const windowDescriptor = descriptors[propName]; | |
// Keep a reference to the current values on window, so they can be accessed through fakeWindow later: | |
if (current === window) { | |
Object.defineProperty(fakeWindow, propName, windowDescriptor); | |
// Some properties on window are non-configurable and cannot be redefined | |
// such as Infinity, NaN, undefined... and unfortunately, location | |
// location could be removed by running in a worker scope instead, like in other answer | |
if (windowDescriptor && !windowDescriptor.configurable) { | |
return; | |
} | |
} | |
// constructor property cannot be redefined, and console must stay so that throwing works | |
// (`throw` internally calls `window.console`) | |
const keepProps = ['constructor', 'console']; | |
if (keepProps.includes(propName)) { | |
return; | |
} | |
// Set *all* properties anywhere on the prototype chain directly onto window | |
// so that implicit references to window properties will fail, | |
// but so will implicit references to anything on the prototypes, like __defineGetter__ | |
const doThrow = () => { | |
throw new ReferenceError(propName + ' is not defined'); | |
}; | |
Object.defineProperty(window, propName, { get: doThrow, set: doThrow }); | |
}); | |
current = Object.getPrototypeOf(current); | |
} | |
return fakeWindow; | |
})(); | |
(() => { | |
// The actual original window instance doesn't have accessible properties | |
// but we can access the values originally on the instance via fakeWindow | |
// and the values on the prototype via Object.getPrototypeOf(fakeWindow.window) | |
const window = new fakeWindow.Proxy( | |
{}, | |
{ | |
getDescriptor(propName) { | |
const { Object } = fakeWindow; | |
let descriptor = Object.getOwnPropertyDescriptor(fakeWindow, propName); | |
let current = Object.getPrototypeOf(fakeWindow.window); | |
while (!descriptor && current !== null) { | |
descriptor = Object.getOwnPropertyDescriptor(current, propName); | |
current = Object.getPrototypeOf(current); | |
} | |
return descriptor; | |
}, | |
get(_, propName) { | |
const descriptor = this.getDescriptor(propName); | |
if (!descriptor) { | |
return; | |
} | |
const { get, value } = descriptor; | |
if (get) { | |
return get.call(fakeWindow.window); | |
} | |
return typeof value !== 'function' | |
? value | |
: function(...args) { | |
// if this was called with a calling context of the *proxy*, call it with a calling context of the true window | |
// else, call with the default calling context | |
return value.apply(this === window ? fakeWindow.window : this, args); | |
}; | |
}, | |
set(_, propName, newVal) { | |
const directDescriptor = fakeWindow.Object.getOwnPropertyDescriptor(fakeWindow, propName); | |
if (directDescriptor) { | |
const { set } = directDescriptor || {}; | |
return set | |
? set.call(fakeWindow.window, newVal) | |
: fakeWindow[propName] = newVal; | |
} | |
const { set } = this.getDescriptor(propName) || {}; | |
return set | |
? set.call(fakeWindow.window, newVal) | |
: fakeWindow[propName] = newVal; | |
} | |
} | |
); | |
// Implicitly referencing or assigning to a window property will throw: | |
try { | |
name = 3; // would refer to window.name normally | |
} catch(e) { console.log('err', e) } | |
try { | |
const foo = name; // would refer to window.name normally | |
} catch(e) { console.log('err', e) } | |
// Can assign and retrieve properties | |
window.foo = 'foo'; | |
window.console.log(window.foo); | |
// Can invoke setter (with the proper calling context) | |
window.onclick = () => window.console.log('click'); | |
// and getter | |
window.console.log('getting onclick', window.onclick); | |
// Can .call | |
window.addEventListener.call(document.querySelector('div'), 'click', () => window.console.log('div clicked')); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment