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


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

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

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

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


5891
2018-05-23 01:36


起源

在Javascript中私有化可能具有挑战性。看到: javascript.crockford.com/private.html - Andrew McGivery
尝试 在JavaScript中定义只读属性。 - RobG
您可以将所有属性定义为 configurable 并在每次想要更改它们时重新配置它们,但这并不比拥有一个属性更好 _size。 - Ry-♦
@AndrewMcGivery - 我知道Crockford文章中描述的选项。我考虑的那个在我的问题的倒数第二段中描述,但都需要从原型中取出方法并将它们移动到我试图避免的构造函数中。 - jfriend00
@RobG - 该答案定义了一个只读属性,没有人可以修改,这不是我需要的。我需要自己的方法才能修改它。 - jfriend00


答案:


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

  • 一个 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 方法。


9
2018-05-23 06:38



+1为第三种方法。 - plalx
第三种方法看起来既安全又功能齐全。我已经有了其他“私有”功能,可以在你的对象上运行 setSize() 函数在我的模块闭包中,所以这适合于。我可能也会做的 _setSize() 方法不是可枚举的,也不是可写的,只是出于整洁的原因。 - jfriend00
在第三种方法中,而不是一种方法 _setSize() 方法,有没有理由不把相同的逻辑放在setter中 size 属性?然后,私人 setSize() 方法就是这么做的 o.size = v 代替 o._setSize(v)。 - jfriend00
@jfriend00我不明白为什么那是不可能的。但是我仍然认为只使用命名约定会更好。使用此解决方案,您仍然需要为您拥有的每个私有变量至少使用两个特权函数,以及大量增加的复杂性。 - plalx
@plax - 它真的不复杂。我已经实现了我对Bergi的第三种方法的增强,它运行良好,实际上是安全的。我必须添加的是一个setter方法 .size 属性和我的模块闭包中的单个函数。如果你有一个对象的很多这些私有变量,那么这可能会变得冗长,并且可能会将这些方法移出原型,他们可以直接访问构造函数闭包中的私有变量,这将是一个更好的折衷方案。但是,我只有一个,所以这个工作干净,而不必离开原型。 - jfriend00


老实说,我发现为了在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不会阻止该对象引用的值的垃圾收集。


4
2018-05-23 01:51



@RobG除非你希望所有实例共享相同的值,否则我看不出它是如何完成的。 - plalx
你的第一个选择看起来像是明显的解决方法。它可以防止意外设置值,即使仍然可以进行恶意设置。我在搜索时遇到了类似你的第二个选项。当对象被垃圾收集时似乎有问题,因为没有析构函数允许你清理对象中的密钥 privates 对象被垃圾收集时的对象。我不能指望ES6所以不能真正使用WeakMaps。 - jfriend00
@jfriend00带有getter的第一个示例和受保护的以下模式将使其更难设置 _size 除非你从MyObj继承: stackoverflow.com/questions/21799353/... - HMR
@jfriend00没有WeakMaps那么第二个解决方案实际上不是一个选择。然而,对于第一个,你说“恶意设置它仍然是可能的”。虽然这是事实,但我相信如果恶意代码在您的域上运行,您应该关注的不仅仅是强制执行成员隐私,因此我认为这不是针对第一个解决方案的有价值的论据。另一个有趣的方法是Bergi提出的第三种解决方案,但在我看来,增加的复杂性超过了优势。 - plalx
因为我正在处理的是ES6对象的polyfill,我的代码的目的是在weakMaps不可用时使用,因此它们不是一个选项。 - jfriend00


我最近一直这样做:

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

思考?


-1
2017-08-21 14:08