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

jfr*_*d00 12 javascript getter private-members defineproperty

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

  1. 一个对象(其中将有许多单独的实例),每个对象都具有只读属性.size,可以通过直接属性读取从外部读取,但不能从外部设置.
  2. .size必须从原型上的某些方法维护/更新该属性(并且应保留在原型上).
  3. 我的API已经由规范定义,因此我无法对其进行修改(我正在为已定义的ES6对象处理polyfill).
  4. 我大部分时间都试图阻止人们不小心在脚下射击,并且实际上不必有防弹只读(尽管防弹越多越好),所以我愿意妥协一些只要obj.size = 3;不允许直接设置,就可以在侧门进入酒店.

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

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

Ber*_*rgi 11

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

  • size不可转让的财产,即有writable:true或没有setter属性
  • 一种改变size反映的价值的方法,这种价值不是.size = …公开的,因此原型方法可以调用它.

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

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

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

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

这两种方法绝对足以防止"射中脚"的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, …);

    …
}());
Run Code Online (Sandbox Code Playgroud)

只要没有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, …);
Run Code Online (Sandbox Code Playgroud)

但是,此模式不安全,因为可以token通过使用恶意_setSize方法将分配应用于自定义对象来窃取此模式.


pla*_*alx 5

老实说,我发现为了在 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
Run Code Online (Sandbox Code Playgroud)

我见过的另一种方法是使用模块模式来创建一个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;

})();
Run Code Online (Sandbox Code Playgroud)

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

然而,使用 ES6 WeakMap这个问题就消失了,甚至简化了设计,因为我们可以使用对象实例作为键,而不是像上面那样使用数字。如果实例被垃圾回收,weakmap 将不会阻止对该对象引用的值进行垃圾回收。