Coder Social home page Coder Social logo

proposal-private-static's Introduction

proposal-private-static

A TC39 follow-on proposal to extend Private syntax to static fields/methods in classes

Motivation and History

The main motivation to allowing static private fields/methods inside classes is to allow common refactorings. This means both switching from public to private and extracting common code into static helper methods should be supported with minimal fuss. For a full write up on the usecase, please see Example of the utility of static private methods.

But, static private cannot be implemented prefectly, as demonstrated by the subclass footgun. Unlike static public, static privates are not inherited through the prototype chain, and resolving the property through object-resolved bindings (eg, this.#x) breaks the brand checking. Further, there is no clear way to inherit static privates without considerable hacks.

One suggested path forward is to rewrite static private in terms of lexically scoped variables and methods, to which they're analagous. This lead to several proposals, including local lexical bindings, Classes 1.1, and static initializer blocks.

However, we never considered just making the static private lexical, and keeping everything else the same. Until now.

Proposal

Static private methods are now lexically-resolved variables and functions, very similar to the local lexical bindings proposal. But, instead of defining a new syntax, we continue to use the static keyword and the # private syntax. Further, static private are forbidden (via early syntax errors) from being used in object-resolved bindings, and instance privates are forbidden from using lexical-resolved bindings.

class Example {
  static #staticField = 1;

  static #staticMethod() {}

  #instanceField = 1;

  #instanceMethod() {}

  constructor() {
    // Static fields are lexically-resolved
    #staticField;
    #staticMethod();

    // Instance fields are object-resolved
    this.#instanceField;
    this.#instanceMethod();


    /**
    // Static fields may not be object-resolved
    this.#staticField // Syntax Error!
    this.#staticMethod() // Syntax Error!

    // Instance fields may not be lexically-resolved
    #instanceField // Syntax Error!
    #instanceMethod() // Syntax Error!
    **/
  }
}
Core usecase with static private proposal
export const registry = new JSDOMRegistry();

export class JSDOM {
  #createdBy;

  #registerWithRegistry() {
    // ... elided ...
  }

  async static fromURL(url, options = {}) {
    normalizeFromURLOptions(options);
    normalizeOptions(options);

    const body = await getBodyFromURL(url);
    return #finalizeFactoryCreated(new JSDOM(body, options), "fromURL");
  }

  static fromFile(filename, options = {}) {
    normalizeOptions(options);

    const body = await getBodyFromFilename(filename);
    return #finalizeFactoryCreated(new JSDOM(body, options), "fromFile");
  }

  static #finalizeFactoryCreated(jsdom, factoryName) {
    jsdom.#createdBy = factoryName;
    jsdom.#registerWithRegistry(registry);
    return jsdom;
  }
}

Interaction with this and super in static private methods

Since static private methods are lexically-resolved and invoked, it doesn't make sense to provide a this context. You may, however, use Function.p.call to provide a this.

Additionally, super is defined just like any other static method, in terms of the constructor's __proto__.

class Base {
  static x = 1;
}

class Example extends Base {
  static #staticMethod() {
    return [this, super.x];
  }

  constructor() {
    #staticMethod(); // => [undefined, 1]
    #staticMethod.call({}); // => [{}, 1]
  }
}

Comparisions with other proposals

Static Initializer Blocks

Static initializer blocks solve the core usecase by allowing arbitrary code to run in the scope of the class during class creation.

let getter, setter;
class C {
  #x;
  
  static {
    // Arbitrary code can be run here.
    foo(bar, baz);
    1 + 1;
    
    // Private helper functions may be hoisted here.
    getter = (obj) => obj.#x;
    setter = (obj, value) => obj.#x = value;
  }
}
Core usecase with static initializer blocks
export const registry = new JSDOMRegistry();

let finalizeFactoryCreated;
export class JSDOM {
  #createdBy;

  #registerWithRegistry() {
    // ... elided ...
  }

  async static fromURL(url, options = {}) {
    normalizeFromURLOptions(options);
    normalizeOptions(options);

    const body = await getBodyFromURL(url);
    return finalizeFactoryCreated(new JSDOM(body, options), "fromURL");
  }

  static fromFile(filename, options = {}) {
    normalizeOptions(options);

    const body = await getBodyFromFilename(filename);
    return finalizeFactoryCreated(new JSDOM(body, options), "fromFile");
  }

  static {
    finalizeFactoryCreated = function(jsdom, factoryName) {
      jsdom.#createdBy = factoryName;
      jsdom.#registerWithRegistry(registry);
      return jsdom;
    }
  }
}

While this does solve the usecase, it's not particularly ergonomic. Keeping an uninitialized list of helper function variables at the beginning of the class feels a bit like a code smell. There are valid reasons to use this kind of pattern (Promises and the resolve binding come to mind).

Still, I would much rather declare a variable and initialize it in the same stroke (let x; /* ... */ x = 1 vs let x = 1; /* ... */).

Local Lexical Bindings

Local lexical bindings allow you to define functions and variables inside the ClassBody.

class C {
  #x;

  function getter(obj) {
    return obj.#x;
  }
  function setter(obj, value) {
    obj.#x = value;
  }
}
Core usecase with static initializer blocks
export const registry = new JSDOMRegistry();

export class JSDOM {
  #createdBy;

  #registerWithRegistry() {
    // ... elided ...
  }

  async static fromURL(url, options = {}) {
    normalizeFromURLOptions(options);
    normalizeOptions(options);

    const body = await getBodyFromURL(url);
    return finalizeFactoryCreated(new JSDOM(body, options), "fromURL");
  }

  static fromFile(filename, options = {}) {
    normalizeOptions(options);

    const body = await getBodyFromFilename(filename);
    return finalizeFactoryCreated(new JSDOM(body, options), "fromFile");
  }

  function finalizeFactoryCreated(jsdom, factoryName) {
    jsdom.#createdBy = factoryName;
    jsdom.#registerWithRegistry(registry);
    return jsdom;
  }
}

This proposal suffers from confusing syntax. It's not entirely clear from the syntax that these are not instances fields/methdos. It's also not clear why only declarations are supported and not arbitrary expressions.

Classes 1.1

The Classes 1.1 proposal is a re-imaging the current fields syntax to create a "maximally minimal syntax".

class C {
  var x;
  hidden getter() {
    return this->x;
  }
  hidden setter(value) {
    this->x = value;
  }

  static {
    // Arbitrary code here
  }
}
Core usecase with static initializer blocks
export const registry = new JSDOMRegistry();

export class JSDOM {
  #createdBy;

  #registerWithRegistry() {
    // ... elided ...
  }

  async static fromURL(url, options = {}) {
    normalizeFromURLOptions(options);
    normalizeOptions(options);

    const body = await getBodyFromURL(url);
    return this->finalizeFactoryCreated(new JSDOM(body, options), "fromURL");
  }

  static fromFile(filename, options = {}) {
    normalizeOptions(options);

    const body = await getBodyFromFilename(filename);
    return this->finalizeFactoryCreated(new JSDOM(body, options), "fromFile");
  }

  hidden finalizeFactoryCreated(jsdom, factoryName) {
    jsdom.#createdBy = factoryName;
    jsdom.#registerWithRegistry(registry);
    return jsdom;
  }
}

This is essentially a combination of the previous two proposals. For the core usecase, the static block initializer is largely unnecessary since we have hidden lexical functions. Unfortunately, this lexical functions have very confusing semantics. The left-hand side of the -> (pointer arrow) can be anything (eg, "hello"->x, which will call the hidden lexical x with "hello" as the this context), but the function is resolved lexically. This totally breaks from the obj.x object-resolve lookup JavaScript developer familiar with, and the normal lexically-resolved x lookup.

proposal-private-static's People

Contributors

jridgewell avatar

Watchers

James Cloos avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.