John Winston
John Winston

Reputation: 1471

How to achieve privacy for value saved in `this` in constructor function?

What options do I have to achieve privacy on values I need to save in this in a constructor function? For example, a simple Stack implementation:

function Stack(){
  this._stack = {}
  this._counter = 0
}

Stack.prototype.push = function (item){
  this._stack[this._counter++] = item
  return this
}

Stack.prototype.pop = function (){
  Reflect.deleteProperty(this._stack, --this._counter);
  return this
}

Stack.prototype.peek = function (){
  return this._stack[this._counter - 1]
}

Stack.prototype.length = function (){
  return Object.values(this._stack).length
}

If these methods are not defined as prototype methods, I can easily private them like this:

function Stack(){
  let _stack = {}
  let _counter = 0

  this.push = function (item){
    _stack[_counter++] = item
    return this
  }

  this.pop = function (){
    Reflect.deleteProperty(_stack, --_counter);
    return this
  }

  this.peek = function (){
    return _stack[_counter - 1]
  }

  this.length = function (){
    return Object.values(_stack).length
  }
}

This way _stack and _counter are not exposed, but then these methods are not on prototype chain.

Is it possible to achieve privacy, while the protected values are saved in this?

Upvotes: 13

Views: 278

Answers (4)

Daniil Loban
Daniil Loban

Reputation: 4381

I created a function that has access to private data and returns a function with a closure containing methods for working with them (keeps everything in one place) external functions serve only as a kind of pointers to the internal functions of the provider.

class Stack{
  constructor(){
    this.provider = this.provider('init', this)
  }  
  provider(type, args){
    const state = {} 
    if (type === 'init'){
      state._stack = [];
      state._counter = 0;
      state.this = args;
    } 
    return function (type, args) {
      switch(type){
        case 'push':
          return _push(args)
        case 'pop':
          return _pop()
        case 'peek':
          return _peek()
        case 'length':
          return _length()
      }
      function _push(item){
        state._stack.push(item)
        return state.this
      }
      function _pop(){
        const item = state._stack.pop()
        console.log(item)
        return state.this
      }
      function _peek(){
        return state._stack[state._stack.length-1]
      }
      function _length(){
        return Object.values(state._stack).length
      }
    }
  }
  push(item){
    return this.provider('push', item)
  }
  pop(){
    return this.provider('pop')
  }
  peek(){
    return this.provider('peek')
  }
  length(){
    return this.provider('length')
  }
}

tests:

s = new Stack();
g = new Stack();

s.push(1).push(2).push(3)
console.log('length s:', s.length()) // 3
s.pop(/* 3 */).pop(/* 2*/)
console.log(s.peek())
s.pop(/* 1 */)
console.log('length s:', s.length()) // 0

g.push('a').push('b').push('c').pop(/* c */).push('d')
g.pop(/* d */)                
console.log('g.peek()', g.peek(), /* b */)
g.pop(/* b */)                
g.pop(/* a */)                                              
console.log('length g:', g.length()) // 0

Upvotes: 3

3limin4t0r
3limin4t0r

Reputation: 21130

This answer does not create private properties. However if the intent of the question is to prevent a user from accidentally accessing "private" properties or to prevent property conflict you can use symbols.

A property conflict happens when your function expects property A, while a library (or any other code) also expects property A but for another purpose.

const Stack = (function () {
  const stack = Symbol("_stack");
  const counter = Symbol("_counter");

  function Stack() {
    this[stack] = {};
    this[counter] = 0;
  }

  Stack.prototype.push = function (item) {
    this[stack][this[counter]++] = item;
    return this;
  };

  Stack.prototype.pop = function () {
    Reflect.deleteProperty(this[stack], --this[counter]);
    return this;
  };

  Stack.prototype.peek = function () {
    return this[stack][this[counter] - 1];
  };

  Stack.prototype.length = function () {
    return Object.values(this[stack]).length;
  };
  
  return Stack;
})();

The above code does not prevent a user from accessing the properties, but makes it somewhat hard. You could still access them using the following code:

const stack = new Stack();
const [_stack, _counter] = Object.getOwnPropertySymbols(stack);
stack[_stack]   // gives access to the stack
stack[_counter] // gives access to the counter

Symbol properties are excluded from a lot of common functions like Object.keys(), Object.values(), Object.entries(), and also from for...in loops.

Upvotes: 4

3limin4t0r
3limin4t0r

Reputation: 21130

MDN provides an example of private properties in their Keyed collections guide.

WeakMap object

The WeakMap object is a collection of key/value pairs in which the keys are objects only and the values can be arbitrary values. The object references in the keys are held weakly, meaning that they are a target of garbage collection (GC) if there is no other reference to the object anymore. The WeakMap API is the same as the Map API.

One difference to Map objects is that WeakMap keys are not enumerable (i.e., there is no method giving you a list of the keys). If they were, the list would depend on the state of garbage collection, introducing non-determinism.

For more information and example code, see also "Why WeakMap?" on the WeakMap reference page.

One use case of WeakMap objects is to store private data for an object, or to hide implementation details. The following example is from Nick Fitzgerald's blog post "Hiding Implementation Details with ECMAScript 6 WeakMaps". The private data and methods belong inside the object and are stored in the privates WeakMap object. Everything exposed on the instance and prototype is public; everything else is inaccessible from the outside world because privates is not exported from the module.

const privates = new WeakMap();

function Public() {
  const me = {
    // Private data goes here
  };
  privates.set(this, me);
}

Public.prototype.method = function () {
  const me = privates.get(this);
  // Do stuff with private data in `me`...
};

module.exports = Public;

Applied to your scenario this could look like this:

const Stack = (function () {
  const privates = new WeakMap();

  function Stack() {
    privates.set(this, { stack: {}, counter: 0 });
  }

  Stack.prototype.push = function (item) {
    const _ = privates.get(this);
    _.stack[_.counter++] = item;
    return this;
  };

  Stack.prototype.pop = function () {
    const _ = privates.get(this);
    Reflect.deleteProperty(_.stack, --_.counter);
    return this;
  };

  Stack.prototype.peek = function () {
    const _ = privates.get(this);
    return _.stack[_.counter - 1];
  };

  Stack.prototype.length = function () {
    const _ = privates.get(this);
    return Object.values(_.stack).length;
  };
  
  return Stack;
})();

Upvotes: 5

quicVO
quicVO

Reputation: 888

Here is an example of using private fields. You can make them static with the static keyword, but that is not necessary in this example.

class test {
    #lol = 29;
    #mas = 15;
    constructor() {
        this.#lol++;
        this.#mas--;
        return {
            value: this.#lol - this.#mas
        };
    }
};
console.log(new test().value); // --> 16

Upvotes: 9

Related Questions