Создайте свойство, доступное только для чтения во внешний мир, но мои методы все еще могут быть установлены

В JavaScript (ES5 +) я пытаюсь выполнить следующий сценарий:

  • Объект (из которого будет много отдельных экземпляров), каждый из которых имеет свойство только для чтения .size, которое может быть прочитано извне посредством прямого чтения свойств, но не может быть установлено извне.
  • Свойство .size должно поддерживаться/обновляться из некоторых методов, которые находятся на прототипе (и должны оставаться на прототипе).
  • Мой API уже определен спецификацией, поэтому я не могу ее изменить (я работаю над polyfill для уже определенного объекта ES6).
  • В основном я стараюсь, чтобы люди случайно не стреляли в ногу, и на самом деле не должны иметь пуленепробиваемый доступ к чтению (хотя, чем более пуленепробиваемым, тем лучше), поэтому я готов для компрометации некоторых при доступе к боковой двери в свойство, пока не разрешено прямое установление obj.size = 3;.

Мне известно, что я могу использовать приватную переменную, объявленную в конструкторе, и настроить getter для ее чтения, но мне пришлось бы перемещать методы, которые должны поддерживать эту переменную с прототипа и объявлять их внутри конструктора также (поэтому они имеют доступ к закрытию, содержащему переменную). Для этого конкретного случая я бы предпочел не использовать мои методы для прототипа, поэтому я ищу, какие могут быть другие варианты.

Какие еще могут быть другие идеи (даже если есть какие-то компромиссы)?

Ответ 1

ОК, поэтому для решения вам нужны две части:

  • a size свойство, которое нельзя присваивать, т.е. с атрибутами writable:true или no 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 = …. Для более сложного подхода мы можем создать общедоступный метод установки экземпляра (закрытия), который может быть вызван только из методов прототипа модуля.

(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.

Ответ 2

Честно говоря, я считаю, что слишком много жертв нужно предпринять для обеспечения подлинной конфиденциальности в JS (если вы не определяете модуль), поэтому я предпочитаю полагаться только на соглашения об именах, такие как this._myPrivateVariable.

Это явный индикатор для любого разработчика, что они не должны получать доступ или изменять этот член напрямую, и он не требует жертвовать преимуществами использования прототипов.

Если вам нужен ваш член 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.

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 эта проблема уходит, и это даже упрощает дизайн, потому что мы можем использовать экземпляр объекта вместо ключа числа, как мы делали выше. Если экземпляр получает сбор мусора, то слабая карта не предотвратит сбор мусора значения, на которое ссылается этот объект.