Skip to content Skip to sidebar Skip to footer

Make A Property That Is Read-only To The Outside World, But My Methods Can Still Set

In JavaScript (ES5+), I'm trying to achieve the following scenario: An object (of which there will be many separate instances) each with a read-only property .size that can be rea

Solution 1:

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:

// declareObject.defineProperty(MyObj.prototype, "size", {
    get: function() { returnthis._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:

functionMyObj() { // Constructor// declareObject.defineProperty(this, "size", {
        writable: false, enumerable: true, configurable: true
    });
}
// assignObject.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;
    functionsetSize(o, v) {
        settable = true;
        o.size = v;
        settable = false;
    }

    functionMyObj() { // Constructor// declarevar size;
        Object.defineProperty(this, "size", {
            enumerable: true,
            get: function() { return size; },
            set: function(v) {
                if (!settable) thrownewError("You're not allowed.");
                size = v;
            }
        });
        …
    }

    // assignsetSize(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) thrownewError("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.

Solution 2:

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.

functionMyObj() {
    this._size = 0;
}

MyObj.prototype = {
    constructor: MyObj,

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

    getsize() { returnthis._size; }
};

var o = newMyObj();

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.

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

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

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

    MyObj.prototype = {
        constructor: MyObj,

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

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

    returnMyObj;

})();

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.

Solution 3:

I've been doing this lately:

// File-scope tag to keep the setters private.classPrivateTag {}
const prv = newPrivateTag();

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

exportdefaultclassFoo
{
  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 instanceofPrivateTag)
          {
            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?

Post a Comment for "Make A Property That Is Read-only To The Outside World, But My Methods Can Still Set"