I don't know why I woke up this morning with two differing TC39 proposals in my head, considering I've never put forward any, but
If anyone thinks this has legs, I'd be happy to write up something more substantial.
Context
JavaScript has long had the concept of "variable declarations are actually object property declarations and identifier references are actually property references", but not everywhere. In other words:
window.Promise === Promise // true
// not possible in strict mode anymore, but
foo = 1
window.foo === foo // true
So, when it comes to global scope and global context, as far as JavaScript is concerned, variables are properties. This proposal is simple: do the same for local scopes.
Syntax
To avoid creating a new reserved word (unless that wouldn't be that much an issue?), I would propose something like a new global Scope
(like Symbol
) with Scope.this
, which would represent the locally-bound object.
In other words:
function foo() {
Scope.this.myVar = 2;
myVar === Scope.this.myVar; // true
}
Note that Scope.this
would be dynamically assigned.
function foo() {
let scope = Scope.this
scope.myVar = 2;
{
Scope.this === scope; // false, it is a dynamic getter
myVar === Scope.this.myVar; // true! Just like regular variable scope, variables exist on the prototype chain
}
}
Why?
Yes, of course, you're asking yourself, why might we need this? The answer is that objects and object properties can do things that plain variable assignment cannot do. Namely: getters and setters.
Consider this example from Vue:
<script setup>
import { customRef, computed } from vue;
function createReactive(scope, name, value) {
customRef((track, trigger) => {
Object.defineProperty(scope, name, {
get() {
track()
return value
},
set(val) {
value = val
trigger()
}
})
})
}
createReactive(Scope.this, 'increment', 1)
increment; // not a syntax error, this is now defined
// adds 1 whenever `increment` changes, but WITHOUT requiring `increment.value`.
const myComputed = computed(() => increment + 1)
</script>
Put aside for a moment that the above isn't very ergonomic—we can get to that in a second—the point is that you could create "reactives" in Vue, React, Svelte, Solid without needing accessors like .value
, or destructured getters / setter functions like React hooks. Right now, you could do that by putting getters / setters on the global (e.g. window
or globalThis
) object, but that would be silly. Instead, what we could do is, like global objects that represent global variables, have local objects that represent local variables. So, it would just be extending the same construct that already exists in JavaScript.
Improving ergonomics
Now that we have the core concept of: variable names are property names throughout JavaScript, not just at the global level, we can improve ergonomics by extending decorators.
// Let's create a reactive decorator
function reactive(value, { kind, name, scope }) {
createRef((track, trigger) => {
if (kind === 'let' || kind === 'var') {
Object.defineProperty(scope, name, {
get() {
track();
return typeof value === 'function' ? value() : value;
},
set(val) {
value = val;
trigger();
}
})
} else if (kind === 'const') {
Object.defineProperty(scope, name, {
get() {
track();
return typeof value === 'function' ? value() : value;
}
})
}
})
}
@reactive
let increment = 1;
@reactive
const plusOne = () => increment + 1;
console.log(plusOne); // 2
increment += 1;
console.log(increment); // 2
console.log(plusOne); // 3! It's reactive!
If anyone feels this has legs, let me know, and I'll be happy to flesh this out with ergonomic examples for React / Vanilla JS etc.
Thanks for feedback!
One addendum: right after I posted it occurred to me that technically, you don't necessarily need Scope.this
in order to do "decorators for vars", but I feel like it might be useful to make them first-order concepts.