This is a proposal to add a new directive, 'use initial'
, to JavaScript. It's goal is to help people write code that's robust against globalThis mutation, through the means of a bit of magic. It works like this.
Say at some point, you've replaced Math.max with the function () => 2
. As soon as you enter a scope with a 'use initial'
directive, any modifications to globalThis will seem to temporarily disappear. While you're in that block, Math.max will get "shadowed" by the engine with the real max function. As soon as you leave the scope, Math.max will go back to how it was before.
function getMax(x, y) {
'use initial'
// The modified max function is being shadowed by
// the real max function.
return Math.max(x, y)
}
Math.max = () => 2
console.log(getMax(2, 3)) // 3
Math.max(2, 3) // 2
If you replace the entire Math object on globalThis with something else, then the original Math object would likewise reappear within a 'use initial'
scope.
function getMax(x, y) {
'use initial'
return Math.max(x, y)
}
globalThis.Math = {}
console.log(getMax(2, 3)) // 3
You can even mutate prototypes, and the initial functions would be accessible from within a 'use initial'
scope.
function addOne(value) {
'use initial'
if (!Array.isArray(value)) throw new Error('Give me an array please.')
return value.map(x => x + 1)
}
Array.prototype.map = () => []
addOne([3, 4]) // [4, 5]
addOne([3, 4]).map(x => x + 1) // []
// It'll even protect you if someone's using the prototype to shadow a built-in
const data = [3, 4]
data.map = () => []
addOne(data) // [4, 5]
class MyArray extends Array {
// This function is on the prototype
map() {
return []
}
// This function is on the instance
map = () => []
}
const data = new MyArray(3, 4)
// Still works
addOne(data) // [4, 5]
All of this demonstrates that replacing a built-in property with a custom one will cause the custom property to be "shadowed" by the built-in one for the duration of this special scope. However, if your customization does not replace any built-in values, that customization will persist.
// In this case, the "x" property is still accessible
// (it's not shadowing a built-in on Object.prototype)
// but the toString() function will become shadowed by the built-in.
doStuff({
x: 2,
toString: () => 'a string'
})
function doStuff(data) {
'use initial'
console.log(data.toString()) // '[object Object]'
// The String() constructor doesn't have 'use initial' applied to it,
// so it's able to see the modified toString() function.
console.log(String(data)) // 'a string'
return data.x
}
The ability to access properties that aren't shadowing built-ins, as demonstrated above, is important. It allows you to, for example, add new features via polyfills, use anything that inherits from Object (or any built-in class) in a normal way, and even add custom properties to functions like people will sometimes do.
Note that 'use initial' only really applies to direct property access via "." and maybe brackets "[]". Lots of other stuff will remain unaffected. This means it's still possible to get access to the functions being shadowed. For example:
function doStuff(data) {
'use initial'
Object(array).map() // 2
Object.entries(array) // ['map', <function>]
}
const array = []
array.map = () => 2
doStuff(array)
This idea also gives you free brand checking. For example, I believe this would allow you to robustly check if something is a Map.
function isMap(value) {
'use initial'
return value instanceof Object && value.toString() === '[object Map]'
}
Anything that's an instance of Object can not shadow their own toString() function within a 'use initial'
scope, so you know when you call .toString()
, you're using a native toString function, thus, you know you can trust it's output to not be forged.
I realize there's a high bar for adding new directives, and I realize there's a fair amount of magic going on in this proposal, but I'm sure all of those people out there who use JavaScript in an extreme fashion, and who write horrendously ugly code in order to prevent prototype mutations from affecting their code would appreciate a directive like this. A directive that would let them write JavaScript in a much more normal fashion, while getting the protection they want. It will also make it much easier for other people to start protecting their libraries in a similar fashion.