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

时间:2022-06-04 06:30:07

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

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

  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. 对象(其中将有许多单独的实例),每个对象具有只读属性.size,可以通过直接读取属性从外部读取,但不能从外部设置。
  3. The .size property must be maintained/updated from some methods which are on the prototype (and should stay on the prototype).
  4. 必须维护/更新原型上的一些方法的.size属性(并且应该保留在原型上)。
  5. 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).
  6. 我的API已经由规范定义,所以我不能修改它(我正在为已经定义的ES6对象处理一个polyfill)。
  7. 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.
  8. 我主要是想防止人们不小心在脚上开枪,也不需要有防弹的易读性(尽管防弹的效果越好,越好),所以我愿意在直接设置obj的前提下,在侧门通道上进行一些妥协。大小= 3;不允许的。

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.

我意识到,我可以使用一个私有构造函数中声明的变量和建立一个getter读它,但是我必须移动方法,需要维护该变量的原型也声明它们在构造函数(所以他们可以访问包含变量)的闭包。对于这种特殊的情况,我不希望把我的方法从原型中去掉,所以我正在寻找其他的选项。

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

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

2 个解决方案

#1


9  

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
  • 一种不可赋值的大小属性,例如:true或no setter属性
  • 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.
  • 一种更改size反映的值的方法,它不是.size =…,而且是公共的,以便原型方法可以调用它。

@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:

@plalx已经展示了一种明显的方法,它使用了第二个“半私有”的_size属性,该属性可以通过大小的getter反映出来。这可能是最简单、最直接的解决方案:

// 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:

另一种方法是使size属性不可写,但可配置,以便您必须对objecter . defineproperty(尽管imho对于辅助函数来说太短)使用“the long way”来设置它的值:

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.

这两种方法绝对足以防止“踩到脚”的大小=…作业。对于更复杂的方法,我们可以构建一个公共的、特定于实例的(闭包)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, …);

    …
}());

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.

但是,此模式并不安全,因为可以通过使用恶意的_setSize方法将分配给自定义对象的代码应用到该标记。

#2


3  

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.

老实说,我发现为了在JS中强制实现真正的隐私(除非您正在定义一个模块),需要付出太多的牺牲,所以我宁愿只依赖诸如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.

如果您需要将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

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.

我看到的另一种方法是使用模块模式来创建一个私有对象映射,它将保存单个实例的私有变量。实例化之后,在实例上分配一个只读的私有密匙,然后使用该密钥从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;

})();

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.

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

#1


9  

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
  • 一种不可赋值的大小属性,例如:true或no setter属性
  • 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.
  • 一种更改size反映的值的方法,它不是.size =…,而且是公共的,以便原型方法可以调用它。

@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:

@plalx已经展示了一种明显的方法,它使用了第二个“半私有”的_size属性,该属性可以通过大小的getter反映出来。这可能是最简单、最直接的解决方案:

// 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:

另一种方法是使size属性不可写,但可配置,以便您必须对objecter . defineproperty(尽管imho对于辅助函数来说太短)使用“the long way”来设置它的值:

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.

这两种方法绝对足以防止“踩到脚”的大小=…作业。对于更复杂的方法,我们可以构建一个公共的、特定于实例的(闭包)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, …);

    …
}());

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.

但是,此模式并不安全,因为可以通过使用恶意的_setSize方法将分配给自定义对象的代码应用到该标记。

#2


3  

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.

老实说,我发现为了在JS中强制实现真正的隐私(除非您正在定义一个模块),需要付出太多的牺牲,所以我宁愿只依赖诸如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.

如果您需要将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

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.

我看到的另一种方法是使用模块模式来创建一个私有对象映射,它将保存单个实例的私有变量。实例化之后,在实例上分配一个只读的私有密匙,然后使用该密钥从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;

})();

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.

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