Coder Social home page Coder Social logo

proposal-private-symbols's Introduction

ECMAScript Proposal: Private Symbols

Overview

This proposal allows for the encapsulation of "private" properties to lexical environments. These are known as private symbols because they behave similarly to regular symbols, but at this time there is no actual reification of private symbols. Private symbols can only be access through base.#priv syntax.

class Example {
  #foo = 1;
  get foo() {
    return this.#foo;
  }
  set foo(foo) {
    this.#foo = foo;
  }
}

A private symbol is semantically identical to a regular symbol, with the following exceptions:

  • Private symbols are not exposed by Object.getOwnPropertySymbols.
  • Private symbols are not copied by Object.assign or object spread.
  • Private symbol-keyed properties are not affected by Object.freeze and Object.seal.
  • Private symbols cause proxies to transparently interact with the target without consulting trap handlers.

See Semantics for more details.

Goals

This proposal is intended to provide a missing capability to JavaScript: the ability to add properties to an object that are inaccessible without a unique and unforgeable key. Moreover, this proposal is intended to be minimal, completely generalized and strictly orthogonal.

  • It is minimal because it introduces the minimum amount of new concepts to the language.
  • It is generalized because it applies to both classes and regular objects.
  • It is orthogonal because it does not attempt to solve any problem other than property encapsulation.

The proposal does not attempt to provide solutions for:

  • Secure branding mechanisms.
  • Static shape guarantees.

These are both easily solvable by using WeakSets.

Some Questions and Answers

Do objects inherit private symbols from their prototypes?

Yes.

Private symbols work just like regular symbols when it comes to prototype lookup. There is nothing new to learn. This means that "private methods" on classes just work, without any additional semantics.

const instance = new Example();
instance.foo;
// => 1

const obj = Object.create(instance)
obj.foo;
// => 1

Why don't Object.assign and the spread operator copy private symbols?

It follows from the definition of Object.assign and the spread operator.

They both work by obtaining a list of property keys from the source object, using the [[OwnPropertyKeys]] internal method. Since we do not want to allow private symbols to leak, we restrict the definition of [[OwnPropertyKeys]] such that it is not allowed to return private symbols.

This is exactly the semantics used by the current private fields proposal.

Why don't Object.freeze and Object.seal affect private symbol-keyed properties?

When an object is frozen or sealed, the object is first marked as non-extensible (meaning new properties cannot be added to it) and then a list of property keys is obtained by calling the object's [[OwnPropertyKeys]] internal method. That list is then used to mark properties as non-configurable (and non-writable in the case of Object.freeze).

Since [[OwnPropertyKeys]] is not allowed to return private symbols, freeze and seal cannot modify any property definitions that are keyed with private symbols.

The fundamental idea is that only the code that has access to the private symbol is allowed to make changes to properties keyed by that symbol.

This requires us to slightly modify the definition of a "frozen" object: An object is "frozen" if it is non-configurable and all of it's own non-private properties are non-configurable and non-writable.

This is exactly the semantics used by the current private fields proposal.

How does this work with Proxies?

Proxies are not able to intercept private symbols, and proxy handlers are not allowed to return any private symbols from the ownKeys trap.

For all of the proxy internal methods that accept a property key, if that property key is a private symbol, then the proxy handler is not consulted and the operation is forwarded directly to the target object, as if there were no handler defined for that trap.

Can private symbols be used for branding?

The purpose of a branding mechanism is to mark objects such that, when presented with an arbitrary object, the code that created the "brand" can determine whether or not the object has been marked. In a typical scenario, objects are branded by constructor functions so that method invocations can check whether the this value is an object that was actually created by the constructor function.

WeakSets are a natural solution to this.

const brand = new WeakSet();
function check(...objects) {
  for (const obj of objects) {
    if (!brand.has(obj)) {
      throw new TypeError('non instance');
    }
  }
}

class Example {
  #foo = 1;

  constructor() {
    brand.add(this);
  }

  equal(obj) {
    check(this, obj);
    return this.#foo === obj.#foo;
  }
}

Does this replace private class fields and methods?

Yes. See Private Symbols, or Private Fields.

Semantics

  • Symbols have an additional internal value [[Private]], which is either true or false. By definition, private symbols are symbols whose [[Private]] value is true.
  • The invariants of [[OwnPropertyKeys]] are modified such that it may not return any private symbols.
  • The definition of [[OwnPropertyKeys]] for ordinary objects (and exotic objects other than proxy) is modified such that private symbols are filtered from the resulting array.
  • The proxy definition of [[OwnPropertyKeys]] is modified such that an error is thrown if the handler returns any private symbols.
  • The proxy definitions of all internal methods that accept property keys are modified such that if the provided key is a private symbol, then the operation is forwarded to the target without consulting the proxy handler.

See Specification Changes for more details.

proposal-private-symbols's People

Contributors

jridgewell avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Forkers

igmat

proposal-private-symbols's Issues

Prototypes and Privacy

Walking up the prototype chain on access to private is something we discussed the last time we looked into "static private" solutions. See the section of that repository for a summary of why we didn't select that option. @ljharb puts it succinctly in #43 (comment) :

it must be possible (and the common, default behavior) that any code whatsoever outside the class declaration, including superclasses or subclasses, can not observe anything about private fields; including their names, their values, their absence, or their mere existence.
@littledan - tc39/proposal-class-fields#183 (comment)

To clarify further, if the word "private" is associated with it, I would expect that statement to apply - thus "private symbols walk the prototype chain" imo is a contradictory sentence.
@ljharb - tc39/proposal-class-fields#183 (comment)

These are both arguing that private symbols are not actually private because they fire observable getPrototypeOf traps (assuming a proxy is somewhere in the prototype chain, or the proxy is the base object).

const obj = { #x: 1 };
const p = new Proxy(obj, {
  getPrototypeOf(target) {
    console.log('observed');
  }
});
const sub = Object.create(p);

// observable prototype lookup
p.#x

// observable prototype lookup
sub.#x

If this is a necessary requirement for privacy, why not just skip the getPrototypeOf trap during prototype lookup? The same as get/set handler tunnel directly to the target, we can define the private symbol lookup algorithm to tunnel directly when traversing the prototype.

Symbol shorthand syntax as reification step

Originally I posted it in tc39/proposal-class-fields#206, but it seems that class-fields were improper place for such proposals.
@jridgewell, I've checked your presentation and it seems that following proposal could be used to adjust your Symbol.private approach. What do you think about that?

Syntax

Declaration

Use public, private (protected as follow-up) as declaration keyword, like var/let/const.

let x; // variable declaration
public #x; // create `Symbol()` and store it in lexically scoped constant `#x`
private #y; // create `Symbol.private()` and store it in lexically scoped constant `#y`

Assigment

Any assignment without receiver leads to early SyntaxError

public #x = 1; // SyntaxError
public #y = Symbol(); // SyntaxError
public [#z] = [Symbol()]; // SyntaxError
#k = 1; // SyntaxError
#l = Symbol(); // SyntaxError
[#m] = Symbol(); // SyntaxError

Proper assignment always has receiver, [] and has no keyword.

public #x;
obj[#x] = 1;

Declaration + computed property syntax in object literals

The true power comes with computed property syntax.

public #x;
existingObj[#y] = 1; // throws because #y isn't declared yet
const obj = {
    [#x]: 1,
    public [#y]: 1,
    private [#z]: 1,
};
const otherObj = {
    private [#x]: 1, // shadows `#x` from outer lexical scope
    [#y]: 1, //  throws because #y isn't declared yet in this scope
    private [#z]: 1,
};

There is another idea to closure scope instead of lexical one - in this case private [#x] in otherObj will throw, since redeclaration of #x is restricted. Both ways make some use-cases easier and others - more complex, but provide same feature set, so it's a discussible question which one to prefer.

Declaration + computed property syntax in classes

Work mostly the same way as for objects.

public #x;
existingObj[#y] = 1; // throws because #y isn't declared yet
class SomeClass {
    [#x] = 1;
    public [#y] = 1;
    private [#z] = 1;
};
class SomeOtherClass = {
    private [#x] = 1; // shadowed `#x` not shared with `SomeClass`
    public [#y] = 1; // another `#y` not shared with `SomeClass`
    private [#z] = 1; // another `#z` not shared with `SomeClass`
};

Simple mental model

# stands for Symbol. Any variable starting with this sign is ALWAYS Symbol. So code like this private #x should be read as private symbol x.

Discussible moments

I used already reserved keywords, since we are safe to use them + they are good fit for such mental model. But, obviously, we could select some others, for example:

let x; // variable declaration
sym #x; // create `Symbol()` and store it in closure scoped constant `#x`
psym #y; // create `Symbol.private()` and store it in closure scoped constant `#y`
// or even
#sym #y;  // create `Symbol.private()` and store it in closure scoped constant `#y`

Possible follow-up proposals

  1. Symbol.protected/Symbol.friend/Symbol.<whatever> and <whatever> #x declaration syntax;
  2. <whatever> #x for 'key' as shorthand for const x = Symbol.for('key');
  3. Probably some others, not discovered yet.

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.