tc39 / proposal-extensions

Extensions proposal for ECMAScript
MIT License
150 stars 6 forks source link

Use cases of static invoking the lexical methods/accessors in the classes #14

Open hax opened 2 years ago

hax commented 2 years ago

One use case of this proposal is providing good syntax for first-class protocol:

protocol MyProtocol {
  foo
  bar() {
    this::foo()  // instead of this[MyProtocol.foo]() which is wordy and unsafe
  }
}

Actually classes also have similar use cases:

class MyClass {
  foo() {
  }
  bar() {
    this.foo() // invoke dynamically, but for many reasons we want to ensure to invoke the original foo
    MyClass.prototype.foo.call(this) // invoke statically, wordy and unsafe
  }
}

If foo is getter/setter, the code will be even more hard: Object.getOwnPropertyDescriptor(MyClass.prototype, 'foo').get.call(this)

This proposal already make it easy:

class MyClass {
  foo() {
  }
  bar() {
    this::MyClass:foo() // invoke statically
  }
}

The syntax is ok, but still not safe enough because MyClass.prototype could be hacked. (another small issue is there are anonymous classes)

To ensure safety, we need to extract the methods/accessors in advance.

class MyClass {
  foo() {
  }
  bar() {
    this::foo() // invoke statically, safely
  }
}
const ::{foo, bar} from MyClass

There are two problems of such pattern, first it make the methods outside of the class scope, second the order is unfortunate because you only can extract methods after class definition.

The solution is simple, just allow classes could invoke all class members directly, like protocol.

class MyClass {
  foo() {
  }
  bar() {
    this::foo()
  }
}

expr::foo() // throw ReferenceError

Use cases/Reasons why we want static invoking in the classes

Currently the most "easy" way for such use cases is write every things as private elements, then wrap them as public. This is cumbersome, introduce extra semantic effect (brand checking) which may not wanted, and introduce perf cost due to wrapper. All such issues prevent the users adopt the pattern.

It's also very useful for shared structs or record, because they only allow data fields and no methods/accessors. We could have a companion class to provide methods/accessors for them, and the methods/accessors should always be invoked statically.


Finally, for consistency we should also support it in object literals, this allow us write extensions just using object literal (this matches the util function namespaces today), and convenient to invoke extension methods/accessors of the same extension (normally, we collect the related methods in one extension, so very possible to invoke one method in the other).