Make a property that is read-only to the outside world, but my methods can still set
Asked Answered
C

3

16

In JavaScript (ES5+), I'm trying to achieve the following scenario:

  1. An object (of which there will be many separate instances) each with a read-only property .size that can be read from the outside via direct property read, but cannot be set from the outside.
  2. The .size property must be maintained/updated from some methods which are on the prototype (and should stay on the prototype).
  3. My API is already defined by a specification so I can't modify that (I'm working on a polyfill for an already-defined ES6 object).
  4. I'm mostly trying to prevent people from shooting themselves in the foot accidentally and don't really have to have bulletproof read-only-ness (though the more bullet-proof it is, the better), so I am willing to compromise some on side door access to the property as long as directly setting obj.size = 3; isn't allowed.

I'm aware that I could use a private variable declared in the constructor and set up a getter to read it, but I would have to move the methods that need to maintain that variable off the prototype and declare them inside the constructor also (so they have access to the closure containing the variable). For this particular circumstance, I'd rather not take my methods off the prototype so I'm searching for what the other options might be.

What other ideas might there be (even if there are some compromises to it)?

Cymograph answered 23/5, 2014 at 1:36 Comment(5)
Making things private in Javascript can be challenging. see: javascript.crockford.com/private.htmlSusurrus
Try Defining read-only properties in JavaScript.Escarpment
You can define all your properties as configurable and reconfigure them each time you want to change them, but that’s not much better than having a property called _size.Antonio
@AndrewMcGivery - I am aware of the options described in Crockford's article. The one I considered is described in the second to last paragraph of my question, but all require taking methods off the prototype and moving them into the constructor which I was trying to avoid.Cymograph
@Escarpment - that answer defines a read-only property that nobody can modify which isn't what I need. I need my own methods to be able to modify it.Cymograph
S
12

OK, so for a solution you need two parts:

  • a size property which is not assignable, i.e. with writable:true or no setter attributes
  • a way to change the value that size reflects, which is not .size = … and that is public so that the prototype methods can invoke it.

@plalx has already presented the obvious way with a second "semiprivate" _size property that is reflected by a getter for size. This is probably the easiest and most straightforward solution:

// declare
Object.defineProperty(MyObj.prototype, "size", {
    get: function() { return this._size; }
});
// assign
instance._size = …;

Another way would be to make the size property non-writable, but configurable, so that you have to use "the long way" with Object.defineProperty (though imho even too short for a helper function) to set a value in it:

function MyObj() { // Constructor
    // declare
    Object.defineProperty(this, "size", {
        writable: false, enumerable: true, configurable: true
    });
}
// assign
Object.defineProperty(instance, "size", {value:…});

These two methods are definitely enough to prevent "shoot in the foot" size = … assignments. For a more sophisticated approach, we might build a public, instance-specific (closure) setter method that can only be invoked from prototype module-scope methods.

(function() { // module IEFE
    // with privileged access to this helper function:
    var settable = false;
    function setSize(o, v) {
        settable = true;
        o.size = v;
        settable = false;
    }

    function MyObj() { // Constructor
        // declare
        var size;
        Object.defineProperty(this, "size", {
            enumerable: true,
            get: function() { return size; },
            set: function(v) {
                if (!settable) throw new Error("You're not allowed.");
                size = v;
            }
        });
        …
    }

    // assign
    setSize(instance, …);

    …
}());

This is indeed fail-safe as long as no closured access to settable is leaked. There is also a similar, popular, little shorter approach is to use an object's identity as an access token, along the lines of:

// module IEFE with privileged access to this token:
var token = {};

// in the declaration (similar to the setter above)
this._setSize = function(key, v) {
    if (key !== token) throw new Error("You're not allowed.");
        size = v;
};

// assign
instance._setSize(token, …);

However, this pattern is not secure as it is possible to steal the token by applying code with the assignment to a custom object with a malicious _setSize method.

Scantling answered 23/5, 2014 at 6:38 Comment(7)
The third approach looks both secure and fully functional. I already have other "private" functions that operate on the object like your setSize() function in my module closure so this fits right in with that. I'd probably also make the _setSize() method not be enumerable or writable or configurable just for tidiness reasons.Cymograph
In the third approach, rather than a _setSize() method, is there any reason not to put the same logic in a setter for the size property? Then, the private setSize() method would just do o.size = v instead of o._setSize(v).Cymograph
@Cymograph I do not see why that would not be possible. However I still think you would be better off just using naming conventions. With this solution you still need at least two priviledged functions for every private variable that you have, plus a lot of added complexity.Spacial
@plax - it's really not complex at all. I've implemented my enhancement of Bergi's 3rd method and it works well and is actually secure. All I had to add was a setter method for the .size property and a single function in my module closure. If you had lots of these private variables for an object, then this could get verbose and maybe moving the methods off the prototype where they can just directly access private variables in the constructor closure would be a better compromise. But, I only have one so this works cleanly without having to move off the prototype.Cymograph
@Cymograph Fair enough for a single private variable, but I do not get why you are so concerned about absolute privacy?Spacial
@Spacial - just trying to write good code and, in a polyfill for an ES6 object, I'm trying to faithfully reproduce the ES6 interface and behavior and I'm curious how to solve this conundrum.Cymograph
@jfriend00: Oh thanks, that's probably a good idea not to have this extra _setSize property around. I'll edit my answer.Scantling
S
5

Honestly, I find that there's too many sacrifices to be made in order to enforce true privacy in JS (unless you are defining a module) so I prefer to rely on naming conventions only such as this._myPrivateVariable.

This is a clear indicator to any developer that they shouldn't be accessing or modifying this member directly and it doesn't require to sacrifice the benefits of using prototypes.

If you need your size member to be accessed as a property you will have no other choice but to define a getter on the prototype.

function MyObj() {
    this._size = 0;
}

MyObj.prototype = {
    constructor: MyObj,

    incrementSize: function () {
        this._size++;
    },

    get size() { return this._size; }
};

var o = new MyObj();

o.size; //0
o.size = 10;
o.size; //0
o.incrementSize();
o.size; //1

Another approach I've seen is to use the module pattern in order to create a privates object map which will hold individual instances private variables. Upon instantiation, a read-only private key gets assigned on the instance and that key is then used to set or retrieve values from the privates object.

var MyObj = (function () {
    var privates = {}, key = 0;

    function initPrivateScopeFor(o) {
       Object.defineProperty(o, '_privateKey', { value: key++ });
       privates[o._privateKey] = {};
    }

    function MyObj() {
        initPrivateScopeFor(this);
        privates[this._privateKey].size = 0;
    }

    MyObj.prototype = {
        constructor: MyObj,

        incrementSize: function () {  privates[this._privateKey].size++;  },

        get size() { return privates[this._privateKey].size; }
    };

    return MyObj;

})();

As you may have noticed, this pattern is interesting but the above implementation is flawed because private variables will never get garbage collected even if there's no reference left to the instance object holding the key.

However, with ES6 WeakMaps this problem goes away and it even simplifies the design because we can use the object instance as the key instead of a number like we did above. If the instance gets garbage collected the weakmap will not prevent the garbage collection of the value referenced by that object.

Spacial answered 23/5, 2014 at 1:51 Comment(5)
@Escarpment Unless you want all instances to share the same value I do not see how it can be done.Spacial
Your first option looks like the obvious work-around. It prevents the accidental setting of the value, even though malicious setting of it is still possible. I came across something like your second option when searching. It seems like it has issues when objects are garbage collected because there's no destructor to allow you to clean up the key in the privates object when an object is garbage collected. I can't count on ES6 so can't really use WeakMaps.Cymograph
@Cymograph The first example with the getter and the following pattern for protected will make it even harder to set _size unless you inherit from MyObj: #21799853Peach
@Cymograph Without WeakMaps then the second solution is not really an option. However with the first one, you said that "malicious setting of it is still possible". While this is true, I believe that you should be concerned about much more than enforcing member privacy if malicious code runs on your domain, so I do not consider it a valuable argument against the first solution. Another interesting approach is the third solution proposed by Bergi, but in my opinion the added complexity outgrows the advantages.Spacial
Since what I'm working on is a polyfill for an ES6 object, my code's purpose is to be used when weakMaps aren't available so they aren't an option.Cymograph
S
0

I've been doing this lately:

// File-scope tag to keep the setters private.
class PrivateTag {}
const prv = new PrivateTag();

// Convenience helper to set the size field of a Foo instance.
function setSize(foo, size)
{
  Object.getOwnPropertyDiscriptor(foo, 'size').set(size, prv);
}

export default class Foo
{
  constructor()
  {
    let m_size = 0;
    Object.defineProperty(
      this, 'size',
      {
        enumerable: true,
        get: () => { return m_size; },
        set: (newSize, tag = undefined) =>
        {
          // Ignore non-private calls to the setter.
          if (tag instanceof PrivateTag)
          {
            m_size = newSize;
          }
        }
      });
  }

  someFunc()
  {
    // Do some work that changes the size to 1234...
    setSize(this, 1234);
  }      
}

I think that covers all of the OP's points. I haven't done any performance profiling. For my use cases, correctness is more important.

Thoughts?

Sandler answered 21/8, 2018 at 14:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.