Skip to content
This repository has been archived by the owner on Jan 25, 2022. It is now read-only.
This repository has been archived by the owner on Jan 25, 2022. It is now read-only.

private and protected are like static and super #90

Closed
d8corp opened this issue Mar 24, 2018 · 26 comments
Closed

private and protected are like static and super #90

d8corp opened this issue Mar 24, 2018 · 26 comments

Comments

@d8corp
Copy link

d8corp commented Mar 24, 2018

I think it'll be great

class X {
  private field = 1
  protected field = 2
  static field = 3
  method () {
    this.x = private.field // equal 1, like this[Symbol('private')].field
    this.y = protected.field // equal 2, like this[Symbol('protected')].field
    this.z = static.field // equal 3, instead this.constructor.field
    this.a = super.field // this.constructor.__proto__.prototype.field (this.__proto__.__proto__.field)
  }
}

Or something like that.
I often use "this.constructor", it'll be nice to use "static" for that.
p.s. the sharp looks ugly

@d8corp d8corp changed the title private and protected are like static private and protected are like static and super Mar 24, 2018
@ljharb
Copy link
Member

ljharb commented Mar 24, 2018

@d8corp how would that work accessing a field on another object - ie, not this?

@d8corp
Copy link
Author

d8corp commented Mar 25, 2018

Another objects should not to have access to private and protected fields because they are PRIVATE. If another object will have an access to private fields then they are not private.
If I understood the question right.

@ljharb
Copy link
Member

ljharb commented Mar 25, 2018

This proposal is “class private”, so code in the class can access private data on any instance of itself. One important and common use case for this is to be able to compare two objects by their private data; another is to brand-check an object using a private field to see if it’s truly an instance of that class.

In other words, it’s private the only way JS ever has privacy - by lexical position in the code.

@d8corp
Copy link
Author

d8corp commented Mar 27, 2018

I think protected field should be available in each instance classes and private field should be available only in current class. This example for understand how it works:

(() => {
  const protectedFields = new WeakMap()
  const privateMarker = Symbol('private')

  const X = (() => {
    function X () {
      protectedFields.set(this, {
        protectedFieldX: 'protected field x'
      })
      X[privateMarker].set(this, {
        privateFieldX: 'private field x'
      })
    }
    X[privateMarker] = new WeakMap()
    X.prototype.test1 = function () {
      const $protected = protectedFields.get(this)
      const $private = X[privateMarker].get(this)

      console.log('X protectedFieldX', this, $protected.protectedFieldX)
      console.log('X privateFieldX', this, $private.privateFieldX)
    }
    return X
  })()

  const Y = (ext => {
    function Y () {
      ext.apply(this)
      protectedFields.set(this, Object.assign({}, protectedFields.get(this), {
        protectedFieldY: 'protected field y'
      }))
      Y[privateMarker].set(this, {
        privateFieldY: 'private field y'
      })
    }
    Y[privateMarker] = new WeakMap()
    Y.prototype.test2 = function () {
      const $protected = protectedFields.get(this)
      const $private = Y[privateMarker].get(this)

      console.log('Y protectedFieldX', this, $protected.protectedFieldX)
      console.log('Y protectedFieldY', this, $protected.protectedFieldY)
      console.log('Y privateFieldX', this, $private.privateFieldX)
      console.log('Y privateFieldY', this, $private.privateFieldY)
    }
    Y.prototype.__proto__ = ext.prototype
    return Y
  })(X)

  const x = new X()
  const y = new Y()
  console.log('-- x.test1 --')
  x.test1()
  console.log('-- y.test1 --')
  y.test1()
  console.log('-- y.test2 --')
  y.test2()
})()

it can look like

class X {
  protected protectedFieldX = 'protected field x'
  private privateFieldX = 'private field x'
  test1 () {
    console.log('X protectedFieldX', this, protected.protectedFieldX)
    console.log('X privateFieldX', this, private.privateFieldX)
  }
}
class Y extends X {
  protected protectedFieldY = 'protected field y'
  private privateFieldY = 'private field y'
  test2 () {
    console.log('Y protectedFieldX', this, protected.protectedFieldX)
    console.log('Y protectedFieldY', this, protected.protectedFieldY)
    console.log('Y privateFieldX', this, private.privateFieldX)
    console.log('Y privateFieldY', this, private.privateFieldY)
  }
}
const x = new X()
const y = new Y()
console.log('-- x.test1 --')
x.test1()
console.log('-- y.test1 --')
y.test1()
console.log('-- y.test2 --')
y.test2()

Use protectedFields and privateMarker to compare this objects

@ljharb
Copy link
Member

ljharb commented Mar 27, 2018

@d8corp since symbols are always fully public, in your implementation, what you've called "private" is just a normal public property, that exposes the WeakMap you call "protected" - making it also fully public.

@Demetri0
Copy link

Demetri0 commented Mar 28, 2018

@ljharb how are you going to get access to 'private' property from @d8corp implementation outside closure?

@shannon
Copy link

shannon commented Mar 28, 2018

@Demetri0 @d8corp All symbol properties are accessible via Object.getOwnPropertySymbols().
So I believe you could get it by just calling Object.getOwnPropertySymbols(Y)

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertySymbols

@d8corp
Copy link
Author

d8corp commented Mar 31, 2018

I just give you an approximate example how to transpile it. You can make it as you wish inside an engine. I don't understand what's a problem to use it.
This example without Symbols

// imagine protectedProps and privateProps are inside js engine and you haven't an access them
const protectedProps = new WeakMap()
const privateProps = new WeakMap()

class X {
  constructor () {
    // imagine js engine does it instead of "protected protectedX = 'protected x'" in body of this class
    protectedProps.set(this, {
      protectedX: 'protected x'
    })
    // imagine js engine does it instead of "private privateX = 'private x'" in body of this class
    privateProps.get(X).add(this, {
      privateX: 'private x'
    })
  }
  test () {
    // imagine js engine gives you access to "protected" instead of $protected
    const $protected = protectedProps.get(this)
    // imagine js engine gives you access to "private" instead of $private
    const $private = privateProps.get(X).get(this)
  }
}
// this is in js engine
privateProps.set(X, new WeakMap())

@thysultan
Copy link

thysultan commented Apr 1, 2018

Related tc39/proposal-private-methods#28

how would that work accessing a field on another object - ie, not this

This might look like private(this).field === private.field, such that this can refer to any instance of the class, the following demonstrates all the valid use cases.

class A {
  // uses the private keyword to create private fields, methods, getters etc.
  private value = 'string' // [1]
  private method () {} // [2]

  // uses the static keyword to create static fields, methods, getters etc
  static value = 'string' // [3]
  static method () {} // [4]

  // uses neiher to create instance fields, methods, getters etc
  value = 'string' // [5]
  method () {} // [6]

  constructor() {
    // invoke instance method
    this['method'](this.value) // [7]

    // use private.name or private['name'] to access private fields, methods, getters etc.
    private['method'](private['value']) // [8]

    // all of the following invocations are equivalent to the previous
    private.method(private.value)
    private(this)['method'](private(this)['value'])
    private(this).method(private(this).value)

    // assign private values
    private.length = 1 // [9]
    private['length'] = 1

    static['method'](static['value']) // [10]
    static.method(static.value)
  }
}

If it where possible to introduce private symbols or something akin to private symbols this could be compiled to/equivalent to the following prototype-based constructor approach.

var PrivateSymbolForValue = Symbol.private('value')
var PrivateSymbolForMethod = Symbol.private('method')
var PrivateSymbolForLength = Symbol.private('length')

function A () {
  this[PrivateSymbolForValue] = 'string' // [1]
  this.value = 'string' // [5]
  this['method']() // [7]
  this[PrivateSymbolForMethod](this[PrivateSymbolForValue]) // [8]
  this[PrivateSymbolForLength] = 1 // [9]
  this.constructor['method'](this.constructor['value']) // [10]
}

A.prototype[PrivateSymbolForMethod] = function () {} // [2]
A.prototype.method = function () {} // [6]

A.value = 'string' // [3]
A.method = function () {} // [4]

Related proposal-about-private-symbol

@d8corp
Copy link
Author

d8corp commented Apr 1, 2018

@thysultan @ljharb This sentences are permanent:

  1. private fields are available only inside same class
  2. protected fields are available only inside same class and derived classes
  3. fields of this are available everywhere

see "Access Control and Inheritance" this
If you want to compare two objects use this.fields, protected and private fields are like usual variables.
You want something like this:

function someFunction () {
  let x = 1
}
someFunction['x'] // 1

@d8corp
Copy link
Author

d8corp commented Apr 1, 2018

@thysultan
about [10]: static['method'] should to equal exactly this.constructor['method'] (I have better solution for performance, now it is not necessarily, the object will be same) because I can use A['method'] without static but if I want to get this static field from derived classes I need to write this.constructor['method'] now.
A['method'] looks like private static field
static['method'] looks like protected static field

@thysultan
Copy link

@d8corp You're right this.constructor['method'] is the better translation. Updated.

@d8corp
Copy link
Author

d8corp commented Apr 2, 2018

What do you think about that?

class X {
  value = 1 // prototype value, if you use "use strict" it can be only primitive
  this value = 2
  private value = 3
  protected value = 4
  static value = 5
  constructor () {
    console.log(this.value) // 2
    console.log(private.value) // 3
    console.log(protected.value) // 4
    console.log(static.value) // 5
  }
}

@futagoza
Copy link

futagoza commented Apr 3, 2018

@d8corp value = 1 and this value = 2 are the same field in the initialized class, aren't they? Having more then one way to assign the same variable is nothing new to JavaScript, but that's just confusing IMO, other then that 👍

@jkrems
Copy link

jkrems commented Apr 3, 2018

@d8corp I think you forgot how to access things on other objects for protected and private. E.g. the compare case.

class X {
  private id = Symbol('unique-id-of-instance'); // does this work? I assume it doesn't..?

  constructor() {
    private.id = Symbol('actually-unique-id'); // okay, writing a constructor does the trick
  }

  isEqualTo(other) {
    // how do I get the private properties of another instance of X?
    return private.id == other.id;
  }
}

const x = new X();
x.isEqualTo(x); // ?

edit: Oh, I see. You removed that feature. Unfortunately that makes it super confusing and also a lot less valuable. The usual expectation is that visibility is by class/type, not by instance/object. Having it per instance means any method involving more than exactly one object needs to leak implementation details.

@d8corp
Copy link
Author

d8corp commented Apr 3, 2018

@futagoza

Having more then one way to assign the same variable is nothing new to JavaScript, but that's just confusing

class X {
  a = 1
  method () {}
  this b = 2
  this method1 () {}
}
const x = new X()
/*
x = {
  b: 2,
  method1 () {},
  __proto__: {
    a: 1,
    method () {}
  }
}
*/

This is not the final version.

@d8corp
Copy link
Author

d8corp commented Apr 3, 2018

@jkrems

Having it per instance means any method involving more than exactly one object needs to leak implementation details.

I think in most cases enough to use opened fields for that.

class X {
  static id = Symbol('unique-id-of-instance')
  [static.id] = true
  isEqualTo(other) {
    return other[static.id]
  }
}

Or Symbol.private('unique-id-of-instance')
But if you really want hide it you can use next example.

const X = (() => {
  const id = new WeakSet()
  return class X {
    constructor () {
      id.add(this)
    }
    isEqualTo(other) {
      return id.has(other)
    }
  }
})()

And also we can come up with something else. For example new space like private, protected and etc. This is very flexible pattern. Also we can stay the idea of # but i think it looks better like
this..field = 1
p.s. thanks, now I've imbued about #. I'll think about that.

@littledan
Copy link
Member

Unfortunately, this use of static fields will not work, since static public field initializers are evaluated after computed property names (so that the initializers could instantiate the class).

@d8corp
Copy link
Author

d8corp commented Apr 4, 2018

@littledan
do you mean

class X {
  static id = Symbol('unique-id-of-instance')
  [this.constructor.id] = true // the this is not available
  this [this.constructor.id] = true // the this is available
  isEqualTo(other) {
    return other[static.id]
  }
}

@littledan
Copy link
Member

For why this proposal focuses on strongly private fields rather than symbols, see this FAQ entry.

@d8corp
Copy link
Author

d8corp commented May 28, 2018

Prototype fields

You can imagine like the field is a variable which is one for each instance.

const field = 'value'

Also we create variables for each instance

const filed1 = 'value'
const field2 = {
  value: 'value'
}

function newClass () {
  let field1 = field1
  let field2 = field2
  // you can change field1 like this.field1 = 'new value'
  field1 = 'new value'
  // and field1 from outside still equals 'value' but you can change an object
  field2.value = 'new value'
  // and field2 from outside changes also
}

But the variables are inside the class and you have an access through this.

class Class {
  field ='value'
  method () {
    return this.field
  }
}

The prototype fields are something between static and public fields. Just you need to understand the prototype fields are single variables for all instances, they are like static fields.

Public fields

class Class {
  public field = 'value'
}

This is the fields which inside newClass function, and you can cover prototype fields.

const field = 'value'

function newClass () {
  let field = 'value'
  field = 'new value'
  // ...
}

But it is senselessly.

// what you see
class Class {
  const field = 'value'
  let field = 'new value'
}
// what you write
class Class {
  field = 'value'
  public field = 'new value'
}
// maybe it's better for compatibility
class Class {
  const field = 'value'
  field = 'new value'
}
// or maybe better to use const for immutable prototype fields

Static fields

Static fields are as usual but you need to understand that the fields can be changed. You can use 'static' inside methods to observe the changes.

class X {
  static field = 'value'
  method () {
    return static.field
  }
}
class Y extends X {
  static field = 'new value'
}
const y = new Y()
y.method() // returns 'new value'

Private fields

Prototype and public fields are available anywhere, if you need private space you can use private fields. They are available only inside body of the class.

class Class {
  private field = 'value'
  method () {
    return private.field
  }
}

I agree with @thysultan, you can use private like a function to get private fields of argument for this class because we have similar symptoms for 'super'.

class Class {
  private id = Symbol('Class id')
  instanceOf (obj) {
    return private(obj).id === private.id
  }
}

Protected fields

class Class {
  protected field = 'value'
  method () {
    return protected.field
  }
}

This is like private but the fields available for extended classes

Composite of fields

It will be nice if i have a possible to use type of field once for a few.

class User {
  exp = 0
  ballance = 0
  public
    tasks = new Map(),
    friends = new Map()
  private
    id = Symbol('user id'),
    achievements = new Map()
  get achievements () {
    return new Map(private.achievements)
  }
  get level () {
    return static.getLevel(this.exp)
  }
  isItMe (obj) {
    static.speak(private(obj).id === private.id ? 'yes' : 'no')
  }
  static
    getLevel (exp) {
      return Math.round(Math.sqrt(exp + 1))
    },
    speak (text) {
      console.log(text)
    }
}

@trusktr
Copy link

trusktr commented Dec 8, 2018

This is a very nice syntax proposal. I like this a lot. It stays true to JavaScript: semantic wording. F.e. async functions have to use await so that things are clear (unlike coroutines in other languages).

I love that private.foo is obviously accessing a private field.

this.#foo is indeed ugly.

@trusktr
Copy link

trusktr commented Dec 8, 2018

@ephys @komaroff can you both explain why you downvoted so we can add the weight of your opinion?

@trusktr
Copy link

trusktr commented Dec 8, 2018

@littledan Can you re-open this? The suggestions here don't strictly dictate anything about implementation, you could easily argue that this can be implemented with Symbols.

The syntax is similar to my lowclass implementation, and if I can make it work in JS (to some extent), then the engine can do better (no limitation there).

@trusktr
Copy link

trusktr commented Dec 8, 2018

If we switch to private.foo, it solves a huge issue that this.#foo has: this.#foo does not leave much room in the spec for protected to be added later if we decide to add it later, while the suggestions here leave plenty of room for protected even if we don't add it now (or never).

It's better to take the safer route in this regard, like many, especially @rdking, have pointed out many times.

@trusktr
Copy link

trusktr commented Feb 4, 2019

When I mentioned above,

The syntax is similar to my lowclass implementation, and if I can make it work in JS (to some extent), then the engine can do better (no limitation there).

By "to some extent" I was referring to the fact that @devsnek pointed out the following way to gain access to protected members.

I replied in the following comments with some ideas on how to prevent it.

The main point is, if I can basically achieve it in JavaScript, the JS engine itself could certainly achieve it because it has vast space in which to hide internals that are inaccessible from JavaScript.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants