tc39 / proposal-private-methods

Private methods and getter/setters for ES6 classes
https://arai-a.github.io/ecma262-compare/?pr=1668
345 stars 37 forks source link

Extending a Base Class. Private reference not allowed? #77

Closed hodonsky closed 4 years ago

hodonsky commented 4 years ago

I'm having this error ONLY when the variable is private. However as this method ends up as part of the class, it should have access to this.# properties.

Case:

// file1.js
export class Base {
  #config = {}
  constructor( config, dep){
    this.#config = { ...config }
    this.#extend( dep)
  },
  #extend(dep) {
   const { name, fn } = dep
    this[name] = fn
  }
}
// file2.js
const funfunFN = function() {
  console.log( this.#config ) // Throws: 'Private name #config is not defined'
}

new Base({},funfunFN)

Any thoughts?

bakkot commented 4 years ago

However as this method ends up as part of the class, it should have access to this.# properties.

That is not how private fields in JS work - if you could access them by adding a method to the class, they wouldn't really be private. Methods which need access to private fields need to be defined as part of the class.

hodonsky commented 4 years ago

Uhhh, that sounds more like a 'protected' variable. If the method is in the instance it should have access to it's private methods and properties...

What you're suggesting is re-implementing the entire class, seems a little ridiculous.

ljharb commented 4 years ago

@hodonsky the semantic you expect is what "protected" might be in a classical inheritance language; in those languages, "private" things are not available to subclasses.

The code inside Base can (and does) install and access private fields on instances of a proper subclass (that's defined with class and extends, and calls super()), but the code inside the subclass can not.

hodonsky commented 4 years ago

It's not a subclass... it's extended by itself on instantiation.

If I had a subclass or an actual extended class, I could pass certain expectations around during instantiation. I'm trying to alleviate the need for that in this particular instance. However, I don't want the consumer to be able to console.log(new Base().config), but I do want access to it in the extended functions

hodonsky commented 4 years ago

Oh, it just hit me why that doesn't work in JS.... getters and setters for values aren't blocked by scope here. Dang...

Thoughts on how I might achieve what I want within lexical scope and JS restrictions?

bathos commented 4 years ago

If you really want a given member to be private, but do want some other trusted code to have access to it, the point of access still needs to be defined within the class body (because that’s the scope of private names). However you could “pull them off” so that they’re only available to the code that runs before they’ve been removed, e.g.:

class Foo {
  #bar;

  static getBar(foo) {
    return foo.#bar;
  }
  static setBar(foo, bar) {
    foo.#bar = bar;
  }
}

const { getBar, setBar } = Foo;

delete Foo.getBar;
delete Foo.setBar;

// code here can use getBar/setBar to indirectly interact with the private #bar member of foo instances

Essentially this is elevating the scope of access to module scope rather than class body scope, though it’s awkward. Note that if you crossed module boundaries — like, actually exported getBar/setBar — privacy would be lost entirely, since anything can import any module (but if you’re doing code transforms like bundling, this may be moot).

hodonsky commented 4 years ago

Yeah... I figured that was my only option. Thanks for your replies

bakkot commented 4 years ago

@hodonsky for your particular case, I'd do something like

// file1.js
export class Base {
  #config = {}
  constructor(config, dep){
    this.#config = { ...config }
    this.#extend( dep)
  }
  #extend(dep) {
   const { name, fn } = dep
    this[name] = fn(() => this.#config, v => { this.#config = v; });
  }
}
// file2.js
const funfunFN = (getConfig, setConfig) => function() {
  console.log( getConfig() )
};

new Base({},{ name: 'funfunFN', fn: funfunFN })