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

时间:2014-05-23 01:36:38

标签: javascript getter private-members defineproperty

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

  1. 一个对象(其中将有许多单独的实例),每个对象都有一个只读属性.size,可以通过直接属性读取从外部读取,但不能从外部设置。
  2. .size属性必须从原型上的某些方法维护/更新(并且应保留在原型上)。
  3. 我的API已经由规范定义,因此我无法对其进行修改(我正在为已定义的ES6对象处理polyfill)。
  4. 我主要是试图阻止人们不小心在脚下射击,并且不需要防弹只读(尽管防弹越多越好),所以只要直接设置obj.size = 3;是不允许的,我愿意妥协一些侧门进入酒店。
  5. 我知道我可以使用在构造函数中声明的私有变量并设置一个getter来读取它,但是我必须移动需要从原型中维护该变量的方法并在里面声明它们构造函数也是(因此他们可以访问包含变量的闭包)。对于这种特殊情况,我宁愿不从原型中取出我的方法,所以我要搜索其他选项可能是什么。

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

3 个答案:

答案 0 :(得分:11)

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

  • 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的“长路”(尽管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

答案 1 :(得分:4)

老实说,我发现为了在JS中强制实施真正的隐私而做出了太多的牺牲(除非你定义了一个模块)所以我更喜欢仅依赖命名约定,例如{{1} }。

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

如果您需要将this._myPrivateVariable成员作为属性进行访问,除了在原型上定义一个getter之外别无选择。

size

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

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

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

答案 2 :(得分:-1)

我最近一直在这样做:

// 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的所有要点。我还没有进行任何性能分析。对于我的用例,正确性更重要。

有想法吗?