在JavaScript(ES5 +)中,我试图实现以下场景:
- 一个对象(其中将有许多单独的实例),每个对象都具有只读属性
.size
可以通过直接属性读取从外部读取,但不能从外部设置。
- 该
.size
必须从原型上的某些方法维护/更新属性(并且应保留在原型上)。
- 我的API已经由规范定义,因此我无法修改它(我正在为已定义的ES6对象处理polyfill)。
- 我主要是试图阻止人们不小心在脚下射击,并且不需要防弹只读(尽管防弹越多越好),所以我愿意妥协一些只要直接设置,就可以在侧门进入酒店
obj.size = 3;
是不允许的。
我知道我可以使用在构造函数中声明的私有变量并设置一个getter来读取它,但是我必须移动需要从原型中维护该变量的方法并在构造函数中声明它们(所以他们可以访问包含变量的闭包。对于这种特殊情况,我宁愿不从原型中取出我的方法,所以我正在寻找其他选项可能是什么。
还有什么其他想法(即使它有一些妥协)?
好的,所以对于解决方案,您需要两个部分:
- 一个
size
不可转让的财产,即有 writable:true
或没有 setter
属性
- 一种改变价值的方法
size
反映,这不是 .size = …
这是公共的,以便原型方法可以调用它。
@plalx已经提出了第二个“半私人”的明显方式 _size
被吸气剂反射的属性 size
。这可能是最简单,最直接的解决方案:
// declare
Object.defineProperty(MyObj.prototype, "size", {
get: function() { return this._size; }
});
// assign
instance._size = …;
另一种方法是制作 size
属性不可写,但可配置,所以你必须使用“漫长的路” Object.defineProperty
(尽管imho对辅助函数来说太短了)在其中设置一个值:
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, …);
然而,这种模式并不安全,因为它有可能偷走 token
通过将带有赋值的代码应用于具有恶意的自定义对象 _setSize
方法。
老实说,我发现为了在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 WeakMap这个问题消失了,它甚至简化了设计,因为我们可以使用对象实例作为键而不是像上面那样的数字。如果实例被垃圾收集,则weakmap不会阻止该对象引用的值的垃圾收集。
我最近一直这样做:
// 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的观点。我没有做任何性能分析。对于我的用例,正确性更重要。
思考?