4

Question:

How would you implement protected properties in ES6 classes in an elegant way? (that can be accessed only from inside the child class)

I am not searching a response like "ES don't have protected/package properties". It is already known. I want a nice and cleaner workaround to emulate protected properties.

I don't want to add security. Only a cleaner exposed interface to all the end users of the API.


Example:

I have the following API: (node)

my-class.js:

let Symbols = {
    _secret: Symbol("_secret")
};
class MyClass {
    constructor() {
        this.public = "This is public";
        this[Symbols._secret] = "This is private";
    }
}
// Set the Symbols to a static propietry so any class can access it and extend it
MyClass[Symbol.for("_Symbols")] = Symbols;
module.exports = MyClass

my-child-class.js:

let MyClass = require("./my-class.js");

// extends protected properties with own properties
Symbols = Object.assign({}, MyClass[Symbol.for("_Symbols")] , {
    _childSecret = Symbol("_childSecret")
});

class MyChildClass extends MyClass {
    constructor() {
        super();
        this[Symbols._childSecret] = "This is also private";
        console.log(this[Symbols._secret]); //logs "this is private"
        console.log(this[Symbols._childSecret]); //logs "this is also private"
    }
}
// Set the Symbols to a static propietry so any class can access it and extend it
MyClass[Symbol.for("_Symbols")] = Symbols;
module.exports = MyChildClass;

To use the class:

let MyChildClass = require("./my-child-class.js");
var c = new MyChildClass();

The advantages:

  • The exposed API is cleaner. And the end user of the API can view the exposed methods.

The problem:

  • The code is "pretty" in the base class, but not that pretty in the child class. Is there any way to improve the order?

  • Anyone that can access to Symbol.for("_Symbols") can access to all the protected/private properties of the API. (EDIT: I don't mind this. This is not a problem for me since if someone want to break the API accessing the internal symbols, it is they fault)

vsync
  • 118,978
  • 58
  • 307
  • 400
Ciberman
  • 580
  • 7
  • 23
  • You cannot restrict access to object properties with currently available and actual standards. "How would you implement protected properties in ES6 classes in an elegant way?" --- just as properties. The protection cost should not be higher than the cost of the data/breaking it. – zerkms Aug 04 '16 at 22:59
  • 2
    "How would you implement protected properties in ES6 classes in an elegant way?" by naming convention. everything you come up with, can be circumvented, so in the end you're only increasing the complexity of your own code. – Thomas Aug 04 '16 at 23:11
  • 2
    "I don't mind this. This is not a problem for me since if someone want to break the API accessing the internal symbols, it is they fault" --- so is it correct, that you don't care and understand that your over-complicated code brings nothing but just complication? So, why do you make it more complicated than necessary then? Make all properties just normal properties and the problem is solved. – zerkms Aug 04 '16 at 23:25
  • I have edited my question. **I am searching a claner way to expose only the public api to end developers. I don't really care about the security.** – Ciberman Aug 04 '16 at 23:27
  • 1
    Public api == methods. Make your properties "normal" and expose as many methods/getters/setters as necessary. – zerkms Aug 04 '16 at 23:28
  • 2
    if you are concerned about polluting the class with several "private" properties, maybe you can create an object called _private, and keep all private props in a single place (as properties of _private) – Hugo Silva Aug 04 '16 at 23:35
  • 1
    @Ciberman, there is no protected in JS **everything you expose is public** period. There's no difference wether you want to expose sth. to a developer or to some sub-class; exposed is exposed. You can use some convention to annotate some properties as private/protected/whatever aka. mess with this/rely on this and your application will eventually blow up. Best you can do is make these properties non-enumerable, so that developers don't accidently stumble across them, but everything else is pretty much just bloat. – Thomas Aug 04 '16 at 23:56
  • @Thomas Thanks! Some cleaner way to make properties non-enumerable in es6 classes? I don't want to expose private properties/methods when the end user inspect my class in the chrome inspector. – Ciberman Aug 05 '16 at 00:00

4 Answers4

3

Declaration: Using modules and symbols is an information hiding technique in ES2015+ (but Class attributes using Symbols will be hidden, not strictly private - as per the OP question and assumption).

A lightweight information hiding can be achieved through a combination of ES2015 modules (which would only export what you declare as exported) and ES2015 symbols. Symbol is a new built-in type. Every new Symbol value is unique. Hence can be used as a key on an object.

If the client calling code doesn't know the symbol used to access that key, they can't get hold of it since the symbol is not exported. Example:

vehicle.js

const s_make = Symbol();
const s_year = Symbol();

export class Vehicle {

  constructor(make, year) {
    this[s_make] = make;
    this[s_year] = year;
  }

  get make() {
    return this[s_make];
  }

  get year() {
    return this[s_year];
  }
}

and to use the module vehicle.js

client.js

import {Vehicle} from './vehicle';
const vehicle1 = new Vehicle('Ford', 2015);
console.log(vehicle1.make); //Ford
console.log(vehicle1.year); // 2015

However, symbols although unique, are not actually private since they are exposed via reflection features like Object.getOwnPropertySymbols...

const vals = Object.getOwnPropertySymbols(vehicle1);
vehicle1[vals[0]] = 'Volkswagon';
vehicle1[vals[1]] = 2013;
console.log(vehicle1.make); // Volkswagon
console.log(vehicle1.year); // 2013

Please bear that in mind, although where obfuscation is enough, this approach might be considered.

arcseldon
  • 35,523
  • 17
  • 121
  • 125
1

Protected properties are possible in ES6 using a variation of the WeakMap method for private properties.

The basic technique is:

  1. Store a private weak reference to instance protected data for each class.
  2. Create the protected data in the super constructor.
  3. Pass the protected data down from the super constructor to the subclass constructor.

Simple demonstration (aiming for clarity but not ideal functionality, see below for an improvement). This sets protected data in the parent class and accesses it in the child class. If no methods expose it nothing outside the classes can access it:

// Define parent class with protected data
const Parent = (()=>{

  const protData = new WeakMap();
  
  class Parent {
    constructor () {
      // Create and store protected data for instance
      protData.set(this,{
        prop: 'myProtectedProperty',
        meth () { return 'myProtectedMethod'; }
      });
      
      // If called as super pass down instance + protected data
      if(new.target!==Parent){
        this.prot = protData.get(this);
      }
    }
    
    setText (text) {
      const prot = protData.get(this);
      prot.text = text;
    }
    
    getText () {
      const prot = protData.get(this);
      return prot.text;
    }
  }
  
  return Parent; // Expose class definition

})();

// Define child class with protected data
const Child = (()=>{

  const protData = new WeakMap();
  
  class Child extends Parent {
    constructor (...args) {
      super(...args);
      protData.set(this,this.prot); // Store protected data for instance
      this.prot = undefined; // Remove protected data from public properties of instance
    }
    
    getTextChild () {
      const prot = protData.get(this);
      return prot.text;
    }
  }
  
  return Child; // Expose class definition

})();

// Access protected data
const child = new Child();
child.setText('mytext');
console.log(child.getText()); // 'mytext'
console.log(child.getTextChild()); // 'mytext'

There are a couple details here that could be improved:

  1. This will not work for further subclasses. We clear the protected data in the first subclass so further constructors will not receive it.
  2. The new instance has 'prot' in its keys. We cleared the property in the subclass constructor but it will still enumerate. It's tempting to use delete here but delete is very slow.

Solving for any number of subclasses is easy. Just leave the protected data in if we were called as super:

if(new.target!==Child)this.prot=undefined;

For the property remnant the solution I like is to create a brand new instance in the base class and use the bound this to pass the instance and protected data separately. Then you have a completely clean instance and no delete performance hits. There are some idioms you have to use in your constructors to make it work but it's fully possible.

Here's a final solution with those issues resolved:

// Protected members in ES6

// Define parent class with protected data
const Parent = (()=>{

  const protData = new WeakMap();
  
  let instanceNum = 0;
  
  class Parent {
  
    constructor (...args) {
      // New instance since we will be polluting _this_
      //   Created as instance of whichever class was constructed with _new_
      const inst = Object.create(this.constructor.prototype);
      // .. do normal construction here *on inst*
      
      // If called as super pass down instance + protected data
      if(new.target!==Parent){
        protData.set(inst,{  // Create and store protected data for instance
          instanceNum: ++instanceNum
        });
        this.inst=inst; // Pass instance
        this.prot=protData.get(inst); // Pass protected data
      }
      
      // If called directly return inst as construction result
      //   (or you could raise an error for an abstract class)
      else return inst;
    }
    
    sayInstanceNum () {
      const prot = protData.get(this);
      console.log('My instance number is: '+prot.instanceNum);
    }
  
    setInstanceNumParent (num) {
      const prot = protData.get(this);
      prot.instanceNum = num;
    }
  
  }
  
  return Parent; // Expose class definition

})();

// Define child class with protected data
const Child = (()=>{

  const protData = new WeakMap();
  
  class Child extends Parent {
  
    constructor (...args) {
      super(...args);
      protData.set(this.inst,this.prot); // Store protected data for instance
      
      // If called directly return inst as construction result,
      //   otherwise leave inst and prot for next subclass constructor
      if(new.target===Child)return this.inst;
    }
    
    celebrateInstanceNum () {
      const prot = protData.get(this);
      console.log('HONKYTONK! My instance number is '+prot.instanceNum+'! YEEHAWW!');
    }
    
    setInstanceNumChild (num) {
      const prot = protData.get(this);
      prot.instanceNum = num;
    }
  
  }
  
  return Child; // Expose class definition

})();

// Define grandchild class with protected data
const Grandchild = (()=>{

  const protData = new WeakMap();
  
  class Grandchild extends Child {
  
    constructor (...args) {
      super(...args);
      protData.set(this.inst,this.prot); // Store protected data for instance
      
      // If called directly return inst as construction result,
      //   otherwise leave inst and prot for next subclass constructor
      if(new.target===Grandchild)return this.inst;
    }
    
    adoreInstanceNum () {
      const prot = protData.get(this);
      console.log('Amazing. My instance number is '+prot.instanceNum+' .. so beautiful.');
    }
    
    setInstanceNumGrandchild (num) {
      const prot = protData.get(this);
      prot.instanceNum = num;
    }
  
  }
  
  return Grandchild; // Expose class definition

})();

// Create some instances to increment instance num
const child1 = new Child();
const child2 = new Child();
const child3 = new Child();
const grandchild = new Grandchild();

// Output our instance num from all classes
grandchild.sayInstanceNum();
grandchild.celebrateInstanceNum();
grandchild.adoreInstanceNum();

// Set instance num from parent and output again
grandchild.setInstanceNumParent(12);
grandchild.sayInstanceNum();
grandchild.celebrateInstanceNum();
grandchild.adoreInstanceNum();

// Set instance num from child and output again
grandchild.setInstanceNumChild(37);
grandchild.sayInstanceNum();
grandchild.celebrateInstanceNum();
grandchild.adoreInstanceNum();

// Set instance num from grandchild and output again
grandchild.setInstanceNumGrandchild(112);
grandchild.sayInstanceNum();
grandchild.celebrateInstanceNum();
grandchild.adoreInstanceNum();
Community
  • 1
  • 1
  • Protected methods could be called in the context of the instance from a public method like this: `prot.myMethod.call(this,arg1,arg2,arg3)` –  Nov 17 '16 at 15:17
0

Use # for private (eg #someProperty), use _ for protected (eg _someProperty), no prefix for public.

aboger
  • 2,214
  • 6
  • 33
  • 47
-1

Your approach is pointless.

Symbols do not provide any security because they are public. You can get them so easily with Object.getOwnPropertySymbols.

So if you don't care about security and just want simplicity, use a normal _secret property.

class MyClass {
  constructor() {
    this.public = "This is public";
    this._secret = "This is private";
  }
}
module.exports = MyClass;
let MyClass = require("./my-class.js");
class MyChildClass extends MyClass {
  constructor() {
    super();
    this._childSecret = "This is also private";
    console.log(this._secret); // "this is private"
    console.log(this._childSecret); // "this is also private"
  }
}
module.exports = MyChildClass;
Oriol
  • 274,082
  • 63
  • 437
  • 513
  • 1
    Mh.. the problem is that end users will view the `_secret` properties, for example, in chrome debugger/console when they `console.log(new MyClass())`. Do you know some workaround for this? – Ciberman Aug 04 '16 at 23:50
  • 2
    @Ciberman, `Object.defineProperty(this, "_secret", { value: "This is private", writable:true /*, enumerable: false (default if undefined) */ })` with ES7 Decorators you could wrap this into a really nice syntax – Thomas Aug 05 '16 at 00:03
  • @Ciberman They can also see your symbols via `Object.getOwnPropertySymbols(new MyClass())`. There is no way to safely store private data in a public instance. You should store it somewhere else. – Oriol Aug 05 '16 at 00:24
  • 4
    @Ciberman: Why wouldn't they be able to view symbol properties in the console? That's what debuggers are for after all, to inspect implementation details. There is no workaround other than "don't use the debugger". – Bergi Aug 05 '16 at 00:26
  • I should mention IDEs such as WebStorm can understand the "_protected pattern", and won't highlight prefixed properties on code completion. – igorsantos07 Mar 30 '18 at 04:03
  • We could put a key symbol outside a class, and both in a module .mjs file. – Константин Ван May 08 '22 at 09:03