└── README.md /README.md: -------------------------------------------------------------------------------- 1 | # Private State for ECMAScript Objects 2 | 3 | Stage 0 Proposal 4 | 5 | Champions: 6 | 7 | Allen Wirfs-Brock and Yehuda Katz 8 | 9 | # Overview 10 | 11 | ## Some Requirements 12 | 13 | - Provide mutable private state for userland objects 14 | - Supports allocation as a contiguous storage block when object is created 15 | - Supports inheritance 16 | - Subclass instances include private state defined by superclasses 17 | - Secure: Only accessible via code using syntactic special forms 18 | - Not accessible via external reflection API 19 | - Not reified via ES MOP 20 | - Symmetrical access and declaration 21 | - Private and protected style access controls 22 | - Should not be less ergonomic to use than declared public fields 23 | - Can be decorated 24 | 25 | ## Nice To Haves 26 | 27 | - Should be significantly more ergonomic to use than declared public fields 28 | - Private state for object literals 29 | - "Friend" access controls 30 | - Static private state 31 | - Private helper functions 32 | 33 | ## Approach 34 | 35 | - Private state modeled after ES2015 Internal Slots 36 | - Allocated when object is created and initially set to `undefined` 37 | - Constructors need to explicitly initialize slots to other values 38 | - Private slots are not properties and have their own distinct access syntax and semantics 39 | 40 | ## Examples 41 | 42 | ### Single Private Slot 43 | 44 | ```js 45 | class { 46 | private #data1; // data1 is the name of a private data slot 47 | // the scope of 'data1' is the body of the class definition 48 | constructor(d) { 49 | // #data1 has the value undefined here 50 | 51 | // # is used to access a private data slot 52 | // #data1 is shorthand for this.#data1 53 | #data1 = d; 54 | } 55 | 56 | // a method that accesses a private data slot 57 | get data() { 58 | return #data1; 59 | } 60 | } 61 | ``` 62 | 63 | A `private` declaration within a class body defines a private data slot and associates a name that can be used to access the slot. Each instance of the class will have a distinct corresponding private data slot that is created and initialized to `undefined` when the object is created. 64 | 65 | ### Referencing an Undeclared Slot 66 | 67 | Within a class definition that lacks an `extends` clause it is a syntax error to try to access a private slot name that has not been explicitly declared within that class definition. 68 | 69 | ```js 70 | class { 71 | constructor(d) { 72 | #data2 = d; 73 | // ***^ Syntax Error: 'data2' is not a private slot name 74 | } 75 | } 76 | ``` 77 | 78 | A different rule for class definition that have an `extends` clause will be described in a later section. 79 | 80 | ### Private Slots Are Lexical and "Class Private" 81 | 82 | The code within a class body is not restricted to referencing private slots of the `this` object. The private slots of any instance of the class may be referenced. 83 | 84 | ```js 85 | class DataObj { 86 | private #data1; 87 | 88 | constructor(d) { 89 | #data1 = d; 90 | } 91 | 92 | // 'another' should be an instance of DataObj 93 | sameData(another) { 94 | return #data1 === another.#data1 95 | } 96 | }; 97 | 98 | let obj1= new DataObj(1); 99 | let obj2 = new DataObj(2); 100 | 101 | console.log( obj1.sameData(obj2) ); // false 102 | consloe.log( obj1.sameData(new DataObj(1)) ); // true 103 | ``` 104 | 105 | The code within static methods may reference the private slots of instance objects. 106 | 107 | ```js 108 | class DataObj { 109 | private #data1; 110 | 111 | constructor(d) { 112 | #data1 = d; 113 | } 114 | 115 | // 'arg1' and 'arg2' must be an instance of DataObj 116 | static sameData(arg1, arg2) { 117 | return arg1.#data1 === arg2.#data1 118 | } 119 | }; 120 | 121 | let obj1 = new DataObj(1); 122 | let obj2 = new DataObj(2); 123 | 124 | console.log( DataObj.sameData(obj1,obj2) ); // false 125 | consloe.log( DataObj.sameData(obj1, new DataObj(1)) ); // true 126 | ``` 127 | 128 | ### Runtime Errors 129 | 130 | It is a run-time error to reference non-existent or inaccessible private slot. 131 | 132 | ```js 133 | // assuming the preceding definition of DataObj 134 | let obj3 = { data1: 2 }; 135 | console.log(DataObj.sameData(obj1, obj3)); // throws a ReferenceError exception 136 | ``` 137 | 138 | This example throws on the access `arg2.#data1` because obj3 does not a private slot `#data1`. Instead it has a property named `"data1"`. Private slots are not properties. A `obj.#data1` private slot access does not access a property named `"data1"` and a `obj.data1` or `obj["data1"]` property access will not access a private slot named `#data1`. 139 | 140 | ### Private Names are Lexical 141 | 142 | They are inaccessible outside of their defining class body. 143 | 144 | ```js 145 | // Assuming the preceding definition of DataObj and that 146 | // the following code is not within the body of `class DataObj` 147 | 148 | let obj4 = new DataObj(4); 149 | 150 | // either early Syntax Error or runtime ReferenceError 151 | // depending upon referencing context 152 | console.log(obj4.#data1); 153 | ``` 154 | 155 | ### Not On The Prototype 156 | 157 | Private slots are not accessible via the prototype chain. 158 | 159 | ```js 160 | class DataObj { 161 | private #data1; 162 | 163 | constructor(d) { 164 | #data1 = d; 165 | } 166 | 167 | static testProtoAccess(proto) { 168 | // private slot on proto is directly accessible 169 | console.log(proto.#data1); 170 | 171 | let child = Object.create(proto); 172 | 173 | // but cannot be indirectly accessed via prototype chain 174 | console.log(child.#data1); 175 | } 176 | } 177 | 178 | //logs 42 and then throws ReferenceError 179 | DataObj.testProtoAccess(new DataObj(42)); 180 | ``` 181 | 182 | ### Not Visible to Nested Classes 183 | 184 | Private slots names are only visible to the direct class they were declared inside of. They are not visible to nested class definitions. 185 | 186 | ```js 187 | class DataObj { 188 | private #data1; 189 | 190 | constructor(d) { 191 | #data1 = d; 192 | } 193 | 194 | static testNestedAccess(pDO) { 195 | // private slot is directly accessible from methods 196 | console.log(pDO.#data1); 197 | 198 | function fGetData1(aDO) { 199 | return aDO.#data1; 200 | } 201 | 202 | // private slot is directly accessible from inner functions 203 | console.log(fGetData1(pDO)); 204 | 205 | class CGetData { 206 | static getData1() { 207 | // pDO is visible to inner class but #data1 is not 208 | return pDO.#data1; 209 | } 210 | } 211 | 212 | // try nested class access to outer class private slot 213 | console.log(CGetData1.getData1()); 214 | } 215 | } 216 | 217 | // Throws ReferenceError during definition of 218 | // nested class CGetData 219 | DataObj.testNestedAccess(new DataObj(42)); 220 | ``` 221 | 222 | ### Not Polymorphic 223 | 224 | Private slots names are not polymorphic across different classes. 225 | 226 | ```js 227 | class DataObj { 228 | // private declaration 1 (PD1) 229 | private #data1; 230 | 231 | constructor(d) { 232 | // reference using PD1 233 | #data1 = d; 234 | } 235 | 236 | static sameData(arg1, arg2) { 237 | // references using PD1 238 | return arg1.#data1 === arg2.#data1; 239 | } 240 | }; 241 | 242 | class NotDataObj { 243 | // private declaration 2 (PD2) 244 | private #data1; 245 | 246 | constructor(d) { 247 | // reference using PD2 248 | #data1 = d; 249 | } 250 | }; 251 | 252 | let obj1= new DataObj(1); 253 | let obj2 = new NotDataObj(1); 254 | 255 | // throws Reference Error 256 | console.log( DataObj.sameData(obj1,obj2) ); 257 | 258 | // Because obj2's private slot is defined by PD2 259 | // but referenced using PD1 260 | ``` 261 | 262 | Private slot access resolution is not solely based upon the *IdentiferName* given to the slot. Instead, each slot is identified by a pair consisting of the *IdentifierName* and a specific `private` declaration of that *IdentifierName* . A reference to a private slot such as `obj.#name` is only valid if `obj` has a private slot named `name` and the same `private` declaration for `name` is in scope for both the definition of the slot and the reference to the slot. 263 | 264 | #### Installed on Subclasses 265 | 266 | Private slot **storage** is inherited, but access is lexical: subclasses cannot access private slots installed by the superclass (but see the discussion below of protected slots). 267 | 268 | ```js 269 | class SuperClass { 270 | // private slot defined in a superclass 271 | private #data1; 272 | constructor(d) { 273 | #data1 = d; 274 | } 275 | 276 | get data() { 277 | return #data1 278 | } 279 | } 280 | 281 | class SubClass extends SuperClass { 282 | private #data2; 283 | 284 | constructor(d1,d2) { 285 | super(d1) 286 | #data2 = d2; 287 | } 288 | 289 | get data2() { 290 | return #data2 291 | } 292 | } 293 | 294 | let subObj = new SubClass(42, 24); 295 | 296 | // inherited method can access inherited slot from subclass instance 297 | console.log( subObj.getData() ); // logs 42 298 | 299 | //subclass method can access subclass defined rivate slot 300 | console.log( subObj.getData2() ); //logs 24 301 | ``` 302 | 303 | Subclass instances are created within their locally defined private slots and with the private slots defined by all of the superclasses of the subclass. However, the inherited private slots are not directly accessible by code ithin the body of the subclass definition. 304 | 305 | ```js 306 | class BadSubClass extends SuperClass { //runtime Reference Error 307 | private #data2; 308 | 309 | constructor(d1,d2) { 310 | super(); 311 | 312 | #data1 = d1; 313 | // ***^ Will cause runtime Reference Error 314 | // during class definition because 'data1' 315 | // is not a private slot name of BadSubClass 316 | #data2 = d2; 317 | } 318 | } 319 | ``` 320 | 321 | #### Private Names Are Lexically Distinct 322 | 323 | Subclass can reuse private slot names used by superclasses. 324 | 325 | ```js 326 | class ReuseSlotNameSubClass extends SuperClass { 327 | // a new private slot 328 | private #data1; 329 | 330 | constructor(d1,d2) { 331 | super(d1); 332 | #data1 = d2; 333 | } 334 | 335 | get data2() { 336 | return #data1 337 | } 338 | } 339 | 340 | let obj = new ReuseSlotNameSubClass(42, 24); 341 | 342 | // inherited method accesses inherited slot named `data1` 343 | console.log( obj.getData() ); // logs 42 344 | 345 | // subclass method accesses distinct subclass slot `data1` 346 | console.log( obj.getData2() ); // logs 24 347 | ``` 348 | 349 | Instances of `ReuseSlotNameSubClass` are created with two private slots, each named `#data1`. However, each slot is associated with a distinct `private` declaration. An `obj.#data1` access chooses one of the two slot slots based upon which `private` declaration is statically visible at the point of access. 350 | 351 | **Rationale:** 352 | 353 | - A subclass should not need to be aware of the inaccessible private slot names used by its superclasses. 354 | - Introducing a new private slot name within a superclass should not break already existing subclass definitions that extend the superclass. 355 | 356 | #### Protected Slot Definition and Access 357 | 358 | A protected data slot is a private slot that *may* be accessed from code within the bodies of subclasses of the class that defined the private slot. 359 | 360 | ```js 361 | class Base { 362 | private #slot1; 363 | protected #slot2; 364 | 365 | constructor (s1,s2) { 366 | #slot1 = s1; 367 | #slot2 = s2; 368 | } 369 | } 370 | ``` 371 | 372 | Within its defining class definition, a `protected` declaration for a data slot is treated just like a `private declaration`. However, declaring a data slot using `protected` makes it available for access from derived subclasses. All of the `protected` date slot names defined by a superclass are automatically included in the scope of each of its subclasses unless the subclass explicitly includes a `private` or `protected` declaration for the name: 373 | 374 | ```js 375 | // see above definition of Base 376 | class Derived extends Base { 377 | getData2() { 378 | // protected #slot2 access inherited from Base 379 | return #slot2; 380 | } 381 | } 382 | 383 | // will produce runtime ReferenceError 384 | class Derived2 extends Base { 385 | getData1() { 386 | // slot1 defined as private rather than protected in Base 387 | return #slot1; 388 | } 389 | } 390 | 391 | class Derived3 extends Base { 392 | // adds an additional private slot that hides inherited slot2 393 | private #slot2; 394 | 395 | getData1() { 396 | // returns undefined since subclass slot2 was not initialized 397 | return #slot2; 398 | } 399 | } 400 | ``` 401 | 402 | When a class definition with an `extends` clause is evaluated all private slot names referenced from within the scope of the class body are checked against the local `private` and `protected` declarations of the class body and the protected slot names provided by the class that is obtained by evaluating the `extends` class. A runtime `ReferenceError` occurs during class definition if any referenced slot name is neither locally defined nor provided by the `extends` clause. 403 | 404 | ## Semantics Sketch 405 | 406 | ### Slot Keys 407 | 408 | Slot keys are are internally used to reference a data slot. Conceptually a slot key consists of a reference to the class that declares the slot and the declared name of the slot. There are many ways that an implementation might actually represent a slot key. For example, it might internally assign a symbol value to each unique slot key. 409 | 410 | **Issue** Should slot keys be site specific or instance specific? 411 | 412 | ### Class constructor function object extensions 413 | 414 | - Each function object that is a class constructor has an additional interal slot named `[[instanceSlots]]` 415 | - The value of the `[[instanceSlots]]` internal slot is an ordered List of all instance data slot keys, including inherited slots. 416 | - The `[[instanceSlots]]` List of a subclass constructor is a new List consisting of the the slot keys for all private and protected data slots declared by the subclass appended to the elements of its superclass' `[[instanceSlots]]` List. 417 | - The size of a constructor's `[[instanceSlots]]` List is the number of data slots that need to be allocated when an instance of that constructor is created. 418 | - Each class constructor has an additional internal slot named `[[protectedSlotMap]]` 419 | - The value of the internal slot is a List of string->slot key pairs, mapping *IdentifierNames* to data slot keys of inheritable “protected” data slots. The List includes entires for protected slot names that are inherited from superclasses. 420 | - A subclass adds to its lexical slot bindings the binding pairs from its superclass' `[[protectedSlotMap]]`. 421 | - But it excludes any binding pairs whose *IdentifierName* is redeclared by a `private` or `protected` declaration within the subclass body. 422 | 423 | ###Slot Access Semantics: *MemberExpression* .# *IdentifierName* 424 | 425 | - GetPrivate(obj,slot key) and SetPrivate(obj, slotkey, value) are new abstraction operations for accessing private data slots. 426 | - they are not available via any reflection API 427 | - Parsing maps *IdentiferName* to a slot key using the class definition specific slot map. 428 | - Creates a Data Slot Reference whose base is the value of *MemberExpresion* and whose referenced name is the slot key. 429 | - “Data Slot Reference” is a new kind of Reference Value 430 | - GetValue(Ref) for Data Slot References returns GetPrivate(GetBase(Ref), GetReferencedName(Ref)) 431 | - PutValue(Ref, W) for Data Slot Reference returns SetPrivate(GetBase(Ref), GetReferencedName(Ref), W) 432 | 433 | ## Possible Design Extensions 434 | 435 | The following features are not part of the core proposal. They are possible extensions that show how the "nice to have" requirements could be addressed and/or show how this proposal could integrate with other pending proposals. 436 | 437 | #### Private Data Slot Initializers 438 | 439 | In a manner similar to the [Class Properties Proposal](https://github.com/jeffmo/es-class-static-properties-and-fields), initialzers could be added to the syntax of `private`/`protected` slot declarations. For example: 440 | 441 | ```js 442 | class Base { 443 | private #slot1 = 42; 444 | protected #slot2 = null; 445 | } 446 | ``` 447 | 448 | The [issues](https://github.com/tc39/tc39-notes/blob/master/es7/2015-09/sept-22.md#54-updates-on-class-properties-proposal) related to evaluation time and ordering of such initializers are significant and essentially the same as for Class Property Initializers. If both features are adopted then the handling of initializers should be consistent between them. 449 | 450 | ### Private Slots in Object Literals 451 | 452 | Allow object literals to define private slots. For example: 453 | 454 | ```js 455 | let obj = { 456 | private #data, 457 | get data() { return this.#data; }, 458 | set data(v) { this.#data=v; } 459 | }; 460 | ``` 461 | 462 | Issues: 463 | 464 | - `protected` probably doesn't make sense since object literals don't really have a way to statically define their "inheritance". 465 | 466 | ### Static Slots in a Class 467 | 468 | Static data slots are private data slots of class constructor function objects. The slot declarations would be prefixed with the `static` keyword. 469 | 470 | Class constructors are similar to object literal values in that they are essentially singleton instances. For this reason, the semantics of static slot inheritance has issues and solutions similar to those that arise for object literals. 471 | 472 | ### Per-Class Lexical Scope 473 | 474 | Private data slots provide per-instance private state but they don't provide any support for encapsulated procedural decomposition of methods. The latter could be accomplished by allowing *FunctionDeclaration* and *GeneratorDeclaration* to occur as a *ClassElement*. 475 | 476 | For example: 477 | 478 | ```js 479 | class Example { 480 | private #slot1, #slot2; 481 | function helper(obj) {return obj.#slot1+obj.#slot2}; 482 | method1() {return helper(this)}; 483 | method2() {return helper(this)*2}; 484 | ... 485 | } 486 | ``` 487 | 488 | Note that class body level functions are only visible within the class body. They are not visible to subclasses. However, a base class could use a protected slot (or a class static data slot, if they exist) when it needs to make such helper functions available to its subclasses. 489 | 490 | This features is essentially the same as part of the [Defensible Classes](http://wiki.ecmascript.org/doku.php?id=strawman:defensible_classes) Stage 0 proposal. 491 | 492 | ### "Friend" Access 493 | 494 | In some situations it is useful to allow two or more classes that are not related via inheritance to accesss the internal state of each other's instances. This could be accomplished by allowing Symbols to be used as slot keys. For example: 495 | 496 | ```js 497 | const sharedSecret = Symbol(); 498 | 499 | class Friendly { 500 | //this class has a slot that it exposes to its friends 501 | private #data[sharedSecret]; //defines a slot that has a Symbol as its slot key 502 | constructor (v) { 503 | this.#data = v; 504 | } 505 | } 506 | 507 | class Friend1 { 508 | //this class has access to the data slot of Friendly instances 509 | 510 | //allows us to say obj.#theirData 511 | friend #theirData[sharedSecret]; 512 | 513 | //access uses the Symbol sharedSecret as the slot key 514 | reportOn(aFriend) {return aFriend.#theirData}; 515 | } 516 | } 517 | 518 | let f1 = new Friendly(42); 519 | let f2 = new Friend1(); 520 | console.log(f2.reportOn(f1); 521 | ``` 522 | 523 | In the above example, `friend` is a contextual keyword when appearing as the first element of a *ClassElement* and preceding an *Identifier*, similar to the handling of `get` and `set`. 524 | 525 | Using this scheme, block scoping and explicit parameterization can be used to to manage and constrain friend-style access to private data slots. 526 | 527 | This is a particularly speculative idea. 528 | --------------------------------------------------------------------------------