Proposal: Local variables as scope object properties

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 :man_shrugging:

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.

A scope object allows dynamic scoping, which many delegates would immediately object to, some for security reasons.

Related: proposal-decorators/EXTENSIONS.md at 8936bfe642c63db0c6565572c4847c120400dac3 · tc39/proposal-decorators · GitHub (let decorators)