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

Yet another approach to a more JS-like syntax #28

Closed zocky closed 6 years ago

zocky commented 6 years ago

I realize I might be beating a dead horse, but it seems to me that I'm far from being the only one who intensely dislikes the use of # for private fields/methods. So here goes:

1) Use the private keyword to introduce private field, methods, setters, getters, exactly as with static. 2) Use bare field and method names without any qualifiers to access private fields and methods within the class's methods, just like we do now in factory functions, i.e. treat them like variables. 3) Use private.name to explicitly access the private fields of this object, if there are conflicting names, just like we do with super. Also use private["name"] for non-ident and calculated field names. 4) Use private(otherObject).name or private(otherObject)["name"]to access the private fields of another object of the same class. For symmetry, private(this).x is the same thing as private(this)["x"] private.x or private["x"] or just x if there's no variable x in scope. 5) In methods, private and private(otherObject) are not valid constructs without being followed by .field or ["field"] and throw syntax errors.

AFAICT, this achieves all the requirements, without introducing a wildly asymmetrical new syntax for private fields, AND gives us a natural way of reading code in human language, solving the naming problem that was brought up on other threads.

littledan commented 6 years ago

Use bare field and method names without any qualifiers to access private fields and methods within the class's methods, just like we do now in factory functions, i.e. treat them like variables.

We've been a bit hesitant to go for proposals like this, as it could be seen as a confusing extension to lexical scope. See https://github.com/zenparsing/js-classes-1.1/issues/18 for related discussion.

Use private.name to explicitly access the private fields of this object, if there are conflicting names, just like we do with super.

Similarly, we've been hesitant to add further structures which implicitly reference this, as it's already hard enough for people to figure out this in debugging.

Use private(otherObject).name to access the private fields of another object of the same class. For symmetry, private(this).x is the same thing as private.x or just x if there's no variable x in scope.

In this case, what would be the value of private(otherObject)? Would it require a literal . after it?

One concern I have about this proposal is that, if you forget to reference things through private and just use ., your code will all work (modulo initializers), just that everything will be public. Making # part of the "name" makes this hazard less likely.

zocky commented 6 years ago

We've been a bit hesitant to go for proposals like this, as it could be seen as a confusing extension to lexical scope. See zenparsing/js-classes-1.1#18 for related discussion.

IMO, it's no more confusing than the fact that other syntax inside a class definition works differently than outside of it. And for beginners, it's probably less confusing than a closure. Anyway, it is exactly what we most commonly do now with factory functions when we want to have the equivalent of private fields.

Similarly, we've been hesitant to add further structures which implicitly reference this, as it's already hard enough for people to figure out this in debugging.

I don't understand this concern. It's exactly the same as with other things that already exist. How is private.foo() more confusing than super.foo()?

In this case, what would be the value of private(otherObject)? Would it require a literal . after it?

Yes. It's not a function call, it's a construct. And it wouldn't be the only one that looks vaguely like a function call, but actually isn't. We don't expect if(...) or for(...) or while(...) after a do block to work or parse exactly like function calls, even if they look vaguely similar.

zocky commented 6 years ago

One concern I have about this proposal is that, if you forget to reference things through private and just use ., your code will all work (modulo initializers), just that everything will be public. Making # part of the "name" makes this hazard less likely.

But you don't actually do that in this proposal. this.x is not the same thing as private.x, and can never be accessed using the same syntax.

zocky commented 6 years ago

I've added a clarification above.

zocky commented 6 years ago

Another thought - the idea that private and public fields should not be in the same namespace (which I follow in this proposal) isn't necessarily wrong, but it does make a very common use case (change a field from public to private or vice versa) a menial and error-prone task, unlike in other languages where it consists of just adding or removing the private keyword in the class definition.

mgtitimoli commented 6 years ago

I was pleasantly surprised about this other way proposed by @zocky, I hope it will get a lot of attention, as it's a really clever and clean way of solving this issue.

mgtitimoli commented 6 years ago

I might agree with avoiding the implicit way of using private members, but for the other way, this is, the explicit one, using private not only for defining, but also for accessing them is a clear goal, as it introduces only one keyword to do everything, and allows using the square bracket notation without having to prefix the identifier with any other symbol which is also something really desirable.

On the other hand, the fact it shares a lot of similarities with how super is used, reduces a lot the learning curve required to make use of this new feature at the same time that it clearly simplifies the amount of stuff needed to teach this to newcomers.

And last but not least, it translates 1 to 1 with a possible "private" function that receives an instance and returns the previously stored "private" properties in a WeakMap, which makes this way super easy to teach and understand as it can be constructed with other existent primitives almost directly.

I'm talking about the following example which could be easily refactored using this new syntax just by removing all the helpers, the constructor (it would be super nice to allow initializers too :slightly_smiling_face:), and renaming priv to private.

const privByInstance = new WeakMap();

const initPriv = (instance, members = {}) => privByInstance.set(instance, members);

const priv = instance => privByInstance(instance).get(instance);

export default class MyClass {
  constructor() {
    initPriv(this, {privA: "A", privB: "B"});
  }

  get privA() {
    return priv(this).privA
  }
}
bakkot commented 6 years ago

The FAQ is intended to address this proposal, which has come up a couple of times. As it says, the main thing for me is that private x for declaration without this.x for access seems like it would be a giant footgun.

mgtitimoli commented 6 years ago

So @bakkot you find private.x for accessing private members not enough clear?

zocky commented 6 years ago

As it says, the main thing for me is that private x for declaration without this.x for access seems like it would be a giant footgun.

Is this a technical objection, or just a matter of taste?

Edit: We also declare static fields with static x but can't access them with this.x, but rather have to use this.constructor.x. (It would be nice if we could access them with static.x, but that's a matter for a different discussion.)

mgtitimoli commented 6 years ago

As far as I can see, none of the items expressed in the FAQ explores the way expressed here, and in the other hand and IMHO if at the time of designing the way super is used, the committee resolved that's clear and easy to understand, then the mechanisms expressed here should surprised anyone.

mgtitimoli commented 6 years ago

FTR: I've just shared this issue in JS Classes 1.1 Proposal as IMHO it addresses in a clean and understandable way the main topics that made that other proposal to emerge.

bakkot commented 6 years ago

@mgtitimoli, @zocky, to be specific: I think any proposal which allows

class A {
  private field;
  constructor() {
    this.field = 0;
  }
}

to create an object with a public field holding 0, is unacceptably confusing. This is true no matter what the syntax for accessing the private field is, as long as it is not this.x (which it can't be for reasons also given in the FAQ).

I think this is a very important constraint, which is why it is the first question in the FAQ.

static doesn't bother me because people familiar with other languages will not be misled about which object gets the field.

Is this a technical objection, or just a matter of taste?

I don't really understand the distinction. My concern is not that I find it personally distasteful but rather that I find it misleading, if that's what you're asking.

mgtitimoli commented 6 years ago

@bakkot I noticed you mentioned this way in your previous message, just to be all aligned, what @zocky proposed is to access private members using the following construction:

private.field = 0;

and not

this.field = 0;

bakkot commented 6 years ago

@mgtitimoli, right, that is the problem. this.field = 0 will continue to be legal syntax; it will just silently create a public field rather than writing to the private field.

mgtitimoli commented 6 years ago

@bakkot Just for you to know, they are currently evaluating using this->field to address the same as this proposal in the JS Classes 1.1 Proposal, and with this what I'm trying to say is that without having gotten too deep into the parsing rules and constraints they have defined there, the use of this->field is very similar to private.field, so this means the issue you wrote could be tackled on some similar way they are planning to tackle it there.

zocky commented 6 years ago

@bakkot - So basically, your objection is that somebody could accidentally introduce a logical error in their program? I struggle to see how that's a strong objection.

Especially since the current proposal includes things like this['#x']being distinct from this.#x, which is equally if not more confusing. And AFAICT, it gives no way to access private fields with calculated names, i.e. no square brackets, thus breaking the existing conventions and introducing new asymmetries.

bakkot commented 6 years ago

@mgtitimoli

That proposal notably does not use private x to declare fields.

@zocky

I think "this feature does not make it significantly more likely programmers will introduce subtle logic errors" is one of the single most important constraints of language design. I know not everyone shares this opinion, but I think it's something I and the rest of TC39 are mostly pretty set on, and you are not super likely to persuade us to give it up.

zocky commented 6 years ago

@bakkot - All I'm saying is that the assumption that it's significantly likely to introduce errors doesn't seem to be backed with anything except an assertion in the FAQ.

In any case, it's a matter of pros and cons. This proposal allows all the functionality that's covered by the other syntax and does it in a clearly defined and unambiguous manner.

In addition,

The one con seems to be the possibility that programmers coming from other languages might misunderstand it.

zocky commented 6 years ago

And for the record, I would personally prefer private x for declaration and this.x for access, because that's how it works in every other language, and is the only way that allows fields to be simply switched between being public and private without having to edit every method that uses them. But that option is explicitly disallowed in the FAQ.

thysultan commented 6 years ago

Another point that might be brought up against the use of a private keyword which https://github.com/zenparsing/js-classes-1.1/ makes an effort to avoid is with TypeScript's differing semantics.

That said the simplicity of this alternative looks both easy to learn, teach, and most of all look at, – and given it has been a reserved keyword in strict mode for a while i just hope that this is not held back solely by the precedence of languages that have differing heuristics with regards to the private keyword.

mgtitimoli commented 6 years ago

The main point IMHO this other way introduces is the alternative to use something different from this in the receiver part of the call, and this hasn't been discussed before.

Ok, there are issues with private, that's fine, let's use self.field (or another keyword), but let's give us a chance to focus on what keyword to use before the dot, and not change the dot for .# or even worse, for ->

mgtitimoli commented 6 years ago

And the other advantage is that with just one keyword we could do both: define what's private and access it.

zocky commented 6 years ago

Food for thought, with plenty of caveats:

Yet another possibility would be to use var instead of private, with the same semantics. It would have the advantage of making it natural to use bare field names, since they are declared the same as variables.

class A {
  var field;
  constructor() {
    field = 0;
  }
  equals (other) {
    return field == var(other).field
  }
}

To provide expected behaviour in other contexts, this could work also for other variables that are theoretically in lexical scope, but are hidden:

function foo() {
  var a = 1;

  function bar() {
     var a = 2;
     console.log(a, var(foo).a) // outputs 2,1
  }
}

I fully realize that this is impractical and possibly inconsistent, because it's hard to polyfill and because the function name can be hidden as well, and because the function name doesn't really refer to the closure but rather to the function itself. But it might help us think.

hax commented 6 years ago

@zocky var(foo).a in your last comment seems odd because . means property lookup. But a is not a property. I would like use var(foo)::a which use :: as a scope operator. But if we introduce an operator here, it could be simply written as foo::a. This just bring me to class1.1 proposal, you just need to change -> to ::.

zocky commented 6 years ago

Let's not get sidetracked with this var thing, it's really a separate matter.

thysultan commented 6 years ago

What would it take to present this alternative as a formal proposal?

littledan commented 6 years ago

@thysultan You'd have to convince a TC39 delegate to champion it. Given the renewed consensus in favor of # in the March 2018 meeting, and the arguments that @bakkot presented above, I think this will be difficult.

thysultan commented 6 years ago

"this feature does not make it significantly more likely programmers will introduce subtle logic errors" is one of the single most important constraints of language design. I know not everyone shares this opinion, but I think it's something I and the rest of TC39 are mostly pretty set on, and you are not super likely to persuade us to give it up.

@bakkot How does this not also apply to this.#x having the same likely-hood of programmers introducing subtle errors?

You'd have to convince a TC39 delegate to champion it. Given the renewed consensus in favor of # in the March 2018 meeting, and the arguments that @bakkot presented above, I think this will be difficult.

@littledan What does renewed consensus mean in this case. Is there any way that parties outside of TC39 can contribute to this process of consensus.

In closing, how does one even begin to address the subjective nature of the argument of...

"this feature does not make it significantly more likely programmers will introduce subtle logic errors"

...given that this argument could be applied to all proposals surrounding private methods depending on who you ask, since what constitutes as both significant and likely is entirely dependent on the observer.

Case in point to what observable degree is it significantly more likely for this proposal to introduce these subtle errors when compared with other proposals, i.e @zocky mentioned some observable traits of this.#x that bear the components of introducing subtle logic errors based on the perceived human nature to incline towards consistency, an overloaded . dot access operator without an identical symmetry with bracket notation access renders this long built up consistency void leading to the afore mentioned issue.

bakkot commented 6 years ago

@thysultan

@bakkot How does this not also apply to this.#x having the same likely-hood of programmers introducing subtle errors?

I believe it is significantly less likely, as I've said.

In closing, how does one even begin to address the subjective nature of the argument of...

A lot of design questions are inherently difficult to answer objectively, unfortunately.

thysultan commented 6 years ago

From the looks of it i think the best outcome would be if a primitive akin to private symbols was introduced in order to allow private properties/methods at a much lower level inclusive of the prototype paradigm that JavaScript is primarily based upon and observe what kind of syntax usage via transpilers emerges within the community, this would at-least introduce some kind of objective measure to supplement some of these largely subjective theoretical arguments.

You'd have to convince a TC39 delegate to champion it.

@littledan A non-TC39 delegate might not know any TC39 delegates, what do you suggest? are there official forums for these sort of things.

thysultan commented 6 years ago

Not sure how i'd get other TC39 delegates to have a look at it but an explainer/strawman of this is presented at https://github.com/thysultan/proposal-private-methods-and-fields with the issue i wanted to highlight that both proposals have.

littledan commented 6 years ago

@thysultan I'm not sure what's new in your explainer there which we didn't fully address above.

hax commented 6 years ago

@bakkot

A lot of design questions are inherently difficult to answer objectively, unfortunately.

Unfortunately it sound just like a excuse to ignore the feedback from the community.

As I commented in https://github.com/zenparsing/js-classes-1.1/issues/26#issuecomment-374171662 , and see many issues try to avoid # in related proposal repos, there are enough evidences that # is very unwelcome and will cause continuous boycott. This will cause the split in the community, and very harmful to the relationship between TC39 and the community.

thysultan commented 6 years ago

@littledan I'm not sure what is mean't by "fully address above". I believe these issues still exist because it hasn't been addressed and the arguments used to try to address these issues assume these issues would somehow be less so.

Considering that the JavaScript community has a confusing love/hate relationship with this and that there are numerous articles that try to explain the confusion that stems from this, and that by design the ability to use bracket notation and computed properties on all objects in JavaScript is as old as the language itself, in light of this, the syntax this.# both removes this consistency and adds confusingly to the overloaded this namespace that is already a source of confusion within the JavaScript community.

I believe all of these points are objective arguments that i hope would not be relegated to the subjective argument that they somehow would be less likely, especially since in contradiction to that, these issues already exist.

glen-84 commented 6 years ago

From the looks of it i think the best outcome would be if a primitive akin to private symbols was introduced in order to allow private properties/methods at a much lower level inclusive of the prototype paradigm that JavaScript is primarily based upon and observe what kind of syntax usage via transpilers emerges within the community, this would at-least introduce some kind of objective measure to supplement some of these largely subjective theoretical arguments.

I agree completely. This is what I tried to suggest here (##val), but the issue was closed 2 days later.

And it's not about having two ways of doing the same thing, it's about making this more low-level functionality, so that a cleaner syntax (for the same thing potentially) could be introduced at a later date.

mmis1000 commented 6 years ago

I though this proposal is compatible with the original if we disallow dynamic property names. private.a in this proposal is equal to this.#a in current proposal. private(that).a in this proposal is equal to that.#a in current proposal. They both can be poly-filled with symbols to some degree or make a strict polyfill with weakMap.

a compare of each proposal and what they may be polyfilled

And they are both known at compile time, so they won't hurt the performance due to dynamic lookup.

it seems private(that)[someVariable] has no equivalent in current proposal.
Since we don't know what the privateFieldName is at compile time,
We can't just translate it to some private symbol or internal slot (by either the js engine or transpiler) that no one except us know. Thus private(that)[someVariable] actually changed the behavior of current proposal

bdistin commented 6 years ago

Oh wow, I have been commenting on the "summary of feedback regarding the # sigil prefix" thread with a strikingly similar approach. Championing consistency and scalability with existing and future access modifiers.

Using the access modifier on the left-hand side, as opposed to using this, seems to solve all consistency problems the # Sigil creates; while also allowing parity for protected and any other future access modifiers. I also find this solution the clearest.

I am glad I am not alone in thinking about using the access modifier to solve the issues the # Sigil creates. Huge support for this issue/approach!

littledan commented 6 years ago

The discussion about this above is interesting to me. My intuition went the other way: If we made this implicit, and used some syntax like private.x which did not have this, I would worry that programmers might get more confused, as it's ultimately a new way to refer to an implicit this. For example, it could come up strangely with callbacks:

class C {
  private x;
  queueWork() {
    setImmediate(function f() {
      alert(private.x);
    });
  }
}

It's not clear, here, whether private.x would refer to the instance that queueWork was called on, or the this value when setImmediate calls f. This is why we deferred the private shorthand proposal to be a potential follow-on.

bdistin commented 6 years ago

I don't think that's surprising that wouldn't work, as it's semantics would be the same as trying to use an instance property (private or not) in a function that's not bound to the instance's context. And with ES6 arrow functions being fairly standard for callbacks now, I don't see that being as big a deal as the other funny things that may or may not work with private in general.

consider:

class C {
  private x;
  queueWork(otherIsntanceOfC) {
    setImmediate(() => {
      alert(otherIsntanceOfC.x);
    });
  }
}

probably should work, but would:

class C {
  private x;
  queueWork(otherIsntanceOfC) {
    setImmediate((function f() {
      alert(private.x); // or this.#x
    }).bind(otherIsntanceOfC));
  }
}

At first thought, you would think that should work right? But then, how should it? It's accessing the private as if it were inside its own class, while technically both outside and inside the class... Clearly, it can't be allowed to just bind any function to the context of an instance, to access its private members. But would that work? To me, it's not very clear whether it should or shouldn't work.

Then add on #x syntax to that (assuming the second example would be valid), and #x looks like a local variable instead of an instance variable. Even though we have an understanding #x should have the equivalence of this.#x, it's hard to read:

class C {
  #x;
  queueWork(otherIsntanceOfC) {
    setImmediate((function f() {
      alert(#x);
    }).bind(otherIsntanceOfC));
  }
}

and understand which #x should be alerted.


While this is a long-winded comment, in conclusion, I don't think private.x is any more confusing in callbacks than what is proposed with this.#x and possibly #x even more so.

Edit: And that all works into how protected access and etc would be implemented too. If we access with private.x in callbacks / in general; when it comes the time we need to add properties to the protected namespace and they can't conflict with private or public (else break encapsulation), it won't be any surprise how protected.x would work. If we use this.#x or #x we are going to need more ASI compatible sigils for protected (and other access modifiers), or we will end up with a weird/inconsistent mix of syntaxes. Which leads to needing decoder rings just to read and write code to remember what is what.

mmis1000 commented 6 years ago

I thought, you can just make private shorthand not withing a arrow function that directly under a class method a syntax error.

Because it isn't really make sense to access the private shorthand in a es3 function. Even es3 this used in this way is a major source of bugs.

SomeClass.prototype.method = function () {
  setTimeout(function () {
    this.anotherMethod() // <= this is usually a bug
  }, 1000)
}

Why should we allow this happen again in a brand new syntax?
Can't we just error out usage that may cause bugs?

// ❌syntax error, not under a class
setTimeout(function () { private.x; }, 1000)

// ❌syntax error, not under a class
setTimeout(()=>{ private.x }, 1000)

class A {
  method a() {
    // ❌syntax error, we don't know what is `this` in the `function`
    setTimeout(function () { private.x; }, 1000) 
  }

  method b() {
    // ⭕️ok, as arrow function pass though `this`
    setTimeout(()=>{ private.x; }, 1000)
  }

  method c() {
    // ⭕️ok, as arrow function pass though `this`
    setTimeout(()=>{ 
      setTimeout(()=>{ 
        private.x; 
      }, 1000)
    }, 1000) 
  }

  method d() {
    // ❌syntax error, we don't know what is `this` in the `function`
    setTimeout(()=>{ 
      setTimeout(function () { // <= why are you inside a es3 function?
        private.x; 
      }, 1000)
    }, 1000) 
  }

  method e() {
    // ⭕️ok, you absolutely want to access private property of `this`, so we just do it
    setTimeout(function () { private(this).x; }.bind(this)) 
  }
}

By just disallow common problematic patterns, you could kill a bunch of bugs before they born.
And in this way, it make it clear that, if you can use shorthand, than you are accessing the correct this. If it complains, then you must do it wrong.
It also make distinguish this comes from another place much easier.


class A {
  private a;

  method() {
    return class B {
       private b;

       anotherMethod() {
         private.a; // ❌ hey, we don't have that private property, what are you doing?
         private(this).a // ⭕️ alright, you said the `this` comes from another place is another instance of A, so I just do it.
         private.b; // ⭕️ yes, we have b
       }
    }
  }
}
bdistin commented 6 years ago

Uhh, I am pretty sure your last example wouldn't actually work @mmis1000 ... this in class B would have to refer to the instance of the class B (like an unbound es3 function). You wouldn't expect this.c = 'foo'; to make a public field c in both instances of classes A and B? So even private(this).a in that case would be undefined or undeclared (whatever the semantics would be).

While yes, I am for private(instance) as the way to access private state of an instance of the same class. (much like how the import keyword for expressions, as a function call, and as an object with property access like import.meta) Using private.property should always have the same implications as private(this).property; even though within the class we should be able to skip some function call/internals by just using private.property.

Edit: typos

mmis1000 commented 6 years ago

Well... The this in method of B can absolutely be a instance of A. JavaScript just allow doing this, so we also allow it. The scope of private keyword here acts completely the same as this. The syntax salt here is just to prevent you from doing some creepy things that accidentally use the wrong this. (the problem littledan said)

Consider the following usage:

class A {
  private a;
  private handlers;

  init() {
    private.handlers = class B {
       static handle() {
         private.a; // ❌ you can't use it here, It is just a syntax salt to prevent you do the wrong thing.
         console.log(private(this).a) // ⭕️ ok
       }
    }

    private.a; // ⭕️ ok, a syntax suger
    private(this).a; // ⭕️ you can still use this, although there is no benefit to do so, use it in this way here only cause confusing

    setTimeout(listenerClass.handler.bind(this), 1000)
  }
}
bdistin commented 6 years ago

That still wouldn't work, this in the class B static method would be class B when unbound or not an instance of B when bound to the A instance. So regardless of bindings, neither private.a nor private(this).a should work in that static handle() example.

mmis1000 commented 6 years ago

It seems you incorrectly understand how this in js works. JS never binds anything actively except the arrow function, static method is just a property on a class constructer and means nothing special at all.
It sounds you come from a java or some class based language background. But js doesn't work in that way.
There is no so called bound and unbound, because it never binds anything actively except the arrow function.

A = class A {
  static a() {console.log(this)}
}
// function A()

A.a()
// function A()

('' || A.a)()
// undefined

('' || A.a).call('test')
// 'test'
bdistin commented 6 years ago

@mmis1000 Please find the repl.it based on your example illuminating exactly what I meant in issuecomment-403544625: https://repl.it/@bdistin/ClassInClassPrivateAccess

Edit: In short, there is nothing you can do in class B to get the private.a field from class A. (even if class B is declared in class A) As it should be (encapsulation is important after all). Therefore, there could never be a time when private(this).prop !== private.prop.

mmis1000 commented 6 years ago

That's because you wrote the wrong the type check there. Just fix the type assert there, you will get the wtf result. why are you checking you self is type of B and access property of A?
https://repl.it/@mmis10002/ClassInClassPrivateAccess.
If you use typescript to write this, it should tell you, B class didn't have that field

bdistin commented 6 years ago

I stand corrected. I have been trying all sorts of iterations in chrome (with harmony enabled ofc) and I have come up with some very unexpected results in V8's implementation of private fields: img

Now I can't say if that's a bug in V8 or even an oversight of the specification, but those results break my interpretation of "hard-encapsulation".

  1. You should not be able to access the private members of a different class (like on left) unless the class chooses to reveal them. I don't think a class contained in another class counts as the containing class explicitly choosing to reveal it's private members.
  2. Assuming a class being inside of another class counts as explicitly choosing to reveal it's private members. Assigning its own private member with the same name should not affect the value or visibility of the containing classes private member with the this of the containing class instance. (like on the right)

I wonder if this is expected behavior from any tc39 members?

bakkot commented 6 years ago

A nested class is part of outer class's code; I would be surprised if it were forbidden to access the outer class's private fields. But if it shadows those field by declaring its own, then referring to that name should be an attempt to access its own private field rather than the private field of the outer class.

By analogy:

function A {
  let a = 'A';
  function B() {
    console.log(a);
  }
  B(); // prints 'a'
}
function A {
  let a = 'A';
  function B() {
    let a = 'B';
    console.log(a);
  }
  B(); // prints 'b'
}

This is exactly the behavior I expect, yes.

mmis1000 commented 6 years ago

@bdistin nested function/class is a natural of js, forbid this makes no sense at all, you are in js not java, there is nothing bound by some class or instance or not, there is only called on some class or instance.
this in js is a read-only local variable that automatically assigned with the object that owns the called method (undefined if the method is not called on anything), not class instance.
Private here is also not private by field, but it is instead private by the property name or so called internal slot.
nested class can access parent class's private field makes sense as it is declared inside the parent class, so there is no one except you can declare it.
You can't use the private field name outside the class definition, so no one is going to break your security things.
I thought you just try to understand how js works in Java way, but it's really not how it works. JavaScript compare to java is just like hotdog compare to dog, and it is also never trying to become another copy of java.