创建一个对外部世界只读的属性,但我的方法仍然可以设置

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

本文关键字:方法 我的 设置 属性 一个 只读 世界 对外部 创建      更新时间:2023-09-26

在JavaScript (ES5+)中,我试图实现以下场景:

  1. 一个对象(将有许多独立的实例),每个对象都具有只读属性.size,可以通过直接属性读取从外部读取,但不能从外部设置。
  2. .size属性必须从原型上的一些方法维护/更新(并且应该留在原型上)。
  3. 我的API已经被一个规范定义了,所以我不能修改它(我正在为一个已经定义的ES6对象做一个多边形)。
  4. 我主要是试图防止人们意外地射击自己的脚,而不是真的有防弹的只读性(虽然它越防弹越好),所以我愿意妥协一些侧门进入财产,只要直接设置obj.size = 3;是不允许的。

我知道我可以使用在构造函数中声明的私有变量并设置getter来读取它,但是我必须将需要维护该变量的方法移出原型并在构造函数中声明它们(因此它们可以访问包含该变量的闭包)。对于这种特殊情况,我宁愿不把我的方法从原型中去掉,所以我正在寻找其他可能的选择。

可能有什么其他的想法(即使有一些妥协)?

好的,对于一个解决方案,您需要两个部分:

  • 一个不可分配的size属性,即有writable:true或没有setter属性
  • 一种改变size反映的值的方法,它不是.size = …,并且是公共的,以便原型方法可以调用它。

@plalx已经给出了第二个"半私有"_size属性的明显方法,该属性由size的getter反映。这可能是最简单和最直接的解决方案:

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

另一种方法是使size属性不可写,但可配置,这样您就必须对Object.defineProperty使用"长路"(尽管对于辅助函数来说甚至太短了)来设置其中的值:

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

这两个方法绝对足以防止"搬起石头砸脚"的size = …分配。对于更复杂的方法,我们可以构建一个公共的、特定于实例的(闭包)setter方法,该方法只能从原型模块作用域的方法中调用。

(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, …);
    …
}());

只要不泄露对settable的封闭访问,这确实是故障安全的。还有一种类似的、流行的、更短的方法是使用对象的标识作为访问令牌,如下所示:

// 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, …);

然而,这种模式是不安全的,因为有可能通过将带有赋值的代码应用于带有恶意_setSize方法的自定义对象来窃取token

老实说,我发现为了在JS中强制执行真正的隐私需要做出太多的牺牲(除非你正在定义一个模块),所以我更喜欢只依赖于命名约定,如this._myPrivateVariable

这对任何开发人员来说都是一个明确的指示,他们不应该直接访问或修改这个成员,并且不需要牺牲使用原型的好处。

如果你需要你的size成员作为一个属性被访问,你将别无选择,只能在原型上定义一个getter。

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

我看到的另一种方法是使用模块模式来创建privates对象映射,该映射将保存单个实例私有变量。实例化后,在实例上分配一个只读私钥,然后使用该私钥设置或检索privates对象的值。

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;
})();

您可能已经注意到,这个模式很有趣,但是上面的实现是有缺陷的,因为私有变量永远不会被垃圾收集,即使没有对持有键的实例对象的引用。

然而,在ES6 WeakMaps中,这个问题消失了,它甚至简化了设计,因为我们可以使用对象实例作为键,而不是像上面那样使用数字。如果实例被垃圾收集,弱映射将不会阻止该对象引用的值的垃圾收集。

我最近一直在这样做:

// 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);
  }      
}

我认为这涵盖了OP的所有要点。我没有做任何性能分析。对于我的用例,正确性更重要。

想法吗?