Skip to content

Instantly share code, notes, and snippets.

@wycats
Created November 1, 2015 00:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save wycats/0adf090f776032ec2023 to your computer and use it in GitHub Desktop.
Save wycats/0adf090f776032ec2023 to your computer and use it in GitHub Desktop.

Private State for ECMAScript Objects

Overview

Some Requirements

  • Provide mutable private state for userland objects
    • Supports allocation as a contiguous storage block when object is created
    • Supports inheritance
    • Subclass instances include private state defined by superclasses
  • Secure: Only accessible via code using syntactic special forms
    • Not accessible via external reflection API
    • Not reified via ES MOP
  • Symmetrical access and declaration
  • Private and protected style access controls
  • Should not be less ergonomic to use than declared public fields
  • Can be decorated

Nice To Haves

  • Should be significantly more ergonomic to use than declared public fields
  • Private state for object literals
  • "Friend" access controls
  • Static private state
  • Private helper functions

Approach

  • Private state modeled after ES2015 Internal Slots
  • Allocated when object is created and initially set to undefined
    • Constructors need to explicitly initialize slots to other values
  • Private slots are not properties and have their own distinct access syntax and semantics

Examples

Single Private Slot

class {
  private #data1;   // data1 is the name of a private data slot
                    // the scope of 'data1' is the body of the class definition .@
  constructor(d) {
    // #data1 has the value undefined here

    // # is used to access a private data slot
    // #data1 is shorthand for this.#data1
    #data1 = d; 
  }

  // a method that accesses a private data slot
  get data() {
    return #data1;
  }
}

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.

Referencing an Undeclared Slot

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.

class {
  constructor(d) {
    #data2 = d;
    // ***^ Syntax Error: 'data2' is not a private slot name
  }
}

A different rule for class definition that have an extends clause will be described in a later section.

Private Slots Are Lexical and "Class Private"

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.

class DataObj {
  private #data1;

  constructor(d) {
    #data1 = d;
  }

  // 'another' should be an instance of DataObj
  sameData(another) {
    return #data1 === another.#data1
  }
};

let obj1= new DataObj(1);
let obj2 = new DataObj(2);

console.log( obj1.sameData(obj2) );            // false
consloe.log( obj1.sameData(new DataObj(1)) );  // true

The code within static methods may reference the private slots of instance objects.

class DataObj {
  private #data1;

  constructor(d) {
    #data1 = d;
  }

  // 'arg1' and 'arg2' must be an instance of DataObj
  static sameData(arg1, arg2) {  
    return arg1.#data1 === arg2.#data1
  }
};

let obj1= new DataObj(1);
let obj2 = new DataObj(2);

console.log( DataObj.sameData(obj1,obj2) );           // false
consloe.log( DataObjameData(obj1, new DataObj(1)) );  // true

Runtime Errors

It is a run-time error to reference non-existent or inaccessible private slot.

// assuming the preceding definition of DataObj
let obj3 = { data1: 2 };
console.log(DataObj.sameData(obj1, obj3));  // throws a ReferenceError exception

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 "data" and a obj.data1 or obj1["data1"] property access will not access a private slot named data1.

Private Names are Lexical

They are inaccessible outside of their defining class body.

// Assuming the preceding definition of DataObj and that
// the following code is not within the body of `class DataObj`

let obj4 = new DataObj(4);

// either early Syntax Error or runtime ReferenceError
// depending upon referencing context
console.log(obj4.#data1);

Not On The Prototype

Private slots are not accessible via the prototype chain.

class DataObj {
  private #data1;

  constructor(d) {
    #data1 = d;
  }

  static testProtoAccess(proto) {
    // private slot is directly accessible
    console.log(proto.#data1);

    let child = Object.create(proto);

    // but cannot be indirectly accessed via prototype chain
    console.log(child.#data1);
  }
}

//logs 42 and then throws ReferenceError
DataObj.testProtoAccess(new DataObj(42));

Not Visible to Nested Classes

Private slots names are only visible to the direct class they were declared inside of. They are not visible to nested class definitions.

class DataObj {
  private #data1;

  constructor(d) {
    #data1 = d;
  }

  static testNestedAccess(pDO) {
    // private slot is directly accessible from methods
    console.log(pDO.#data1);

    function fGetData1(aDO) {
       return aDO.#data1;
    }

    // private slot is directly accessible from inner classes
    console.log(fGetData1(pDO));

    class CGetData {
      static getData1() {
        // pDO is visible to inner class but #data1 is not
        return pDO.#data1;
      }
    }

    // try nested class access to outer private slot
    console.log(CGetData1.getData1());
  }
}

// Throws ReferenceError during definition of
// nested class CGetData
DataObj.testNestedAccess(new DataObj(42));

Not Polymorphic

Private slots names are not polymorphic across different classes.

class DataObj {
  // private declaration 1 (PD1)
  private #data1;

  constructor(d) {
    // reference using PD1
    #data1 = d;
  }

  static sameData(arg1, arg2) {
    // references using PD1
    return arg1.#data1 === arg2.#data1;
  }
};

class NotDataObj {
  // private declaration 2 (PD2)
  private #data1;

  constructor(d) {
    // reference using PD2
    #data1 = d;
  }
};

let obj1= new DataObj(1);
let obj2 = new NotDataObj(1);

// throws Reference Error
console.log( DataObj.sameData(obj1,obj2) );

// Because obj2's private slot is defined by PD2
// but referenced using PD1

Private slot 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.

Installed on Subclasses

Private slot storage is inherited, but access is lexical: subclasses cannot access private slots installed by the superclass.

class SuperClass {
  // private slot defined in a superclass
  private #data1;
  constructor(d) {
    #data1 = d;
  }

  get data() {
    return #data1
  }
}

class SubClass extends SuperClass {
  private #data2;

  constructor(d1,d2) {
    super(d1)
    #data2 = d2;
  }

  get data2() {
    return #data2
  }
}

let subObj = new SubClass(42, 24);
console.log( subObj.getData() ); // logs 42

// inherited method can access inherited slot
console.log( subObj.getData2() ); //logs 24

//subclass method can access subclass private slot

Subclass instances are created with 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 within the body of the subclass.

class BadSubClass extends SuperClass { //runtime Reference Error
  private #data2;

  constructor(d1,d2) {
    super();

    #data1 = d1;
    // ***^ Will cause runtime Reference Error
    // during class definition because 'data1'
    // is not a private slot name of BadSubClass
    #data2 = d2;
  }
}

Private Names Are Lexically Unique

Subclass can reuse private slot names used by superclasses.

class ReuseSlotNameSubClass extends SuperClass {
  // a new private slot
  private #data1;

  constructor(d1,d2) {
    super(d1);
    #data1 = d2;
  }

  get data2() {
    return #data1
  }
}

let obj = new ReuseSlotNameSubClass(42, 24);
console.log( obj.getData() ); // logs 42

// inherited method accesses inherited slot named `data1`
console.log( obj.getData2() ); // logs 24

// subclass method accesses distinct subclass private
// slot name `data1`

Instances of ReuseSlotNameSubClass are created with two private slots, each named #data1. However, each slot is associated with a distinct private declaration. A obj.#data1 access choose one of the two slot slots based upon which private declaration is statically visible at the point of access.

Rationale:

  • A subclass should not need to be aware of the inaccessible private slot names used by its superclasses.
  • Introducing a new private slot name within a superclass should not break already existing subclass definitions that extend the superclass.

Protected Slot Definition and Access

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.

class Base {
  private #slot1;
  protected #slot2;

  constructor (s1,s2) {
    #slot1 = s1;
    #slot2 = s2;
  }
 }

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:

// see above definition of Base
class Derived extends Base {
  getData2() {
    // protected #slot2 access inherited from Base  
    return #slot2;  
  }
}

// will produce runtime ReferenceError
class Derived2 extends Base {
  getData1() {
    // slot1 defined as private rather than protected in Base
    return #slot1;
  }

class Derived3 extends Base {
  // adds an additional private slot that hides inherited slot2
  private #slot2;

  getData1() {
    // returns undefined since subclass slot2 was not initialized
    return #slot2;
  }
}

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.

Semantics

Slot Keys

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.

Issue Should slot keys be site specific or instance specific?

Class constructor function object extensions

  • Each class constructor has an additional interal slot named [[instanceSlots]]
    • The value of the [[instanceSlots]] internal slot is an ordered List of all instance data slot keys, including inherited slots.
    • The [[instanceSlots]] List of a subclass 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.
    • The size of the [[instanceSlots]] List is the number of data slots that need to be allocated when an instance of the constructor is created.
  • Each class constructor has an additional internal slot named [[protectedSlotMap]]
  • 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, includes protected slot names that are inherited
  • A subclass adds to its slot bindings the binding pairs from its superclass' [[protectedSlotMap]].

Design Alternatives

Explicit "import" of protected slots

If a derived subclass needs to access a protected data property defined by one of its super classes, it needs to explicit declare that intent:

class Derived extends Base {
  protected super:#slot2;

  get data2() {
    return #slot2;
  }
}

The protected declaration of super:slot2 means that within the body of the enclosing class slot2 has the same private slot binding as it does within the superclass of the enclosing class. In this example, that is the binding of slot2 within the class Base. The declaration does not So, when the access to this.#slot2 will access the private slot named slot2 that is declared and initialized by the class Declaration for Base.

A runtime ReferenceError exception is thrown if a subclass class definition contains a declaration such as protected super:#slot2; and its superclass does not have a visible protected slot binding named slot2. However, such an inherited slot binding does not need to come come from the immediate superclass. For example, the following is valid assuming the above definition of Base:

// intentionally empty body
class Derived1 extends Base { };

class Derived2 extends Derived1 {
  // protected slot2 indirectly inherited from Base
  protected super:#slot2;
  get data2() {
    return this.@slot2;
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment