Развлечения с марками. Эпизод 21. Приватные данные в JavaScript. 4 способа использования штампов

Привет. Я разработчик Василий Боровяк и добро пожаловать в двадцать первую серию Василия Боровяка, представляющего Fun with Stamps.

Нажмите здесь, чтобы опубликовать эту статью в LinkedIn »

Очень часто люди задают вопрос - как иметь частные (или защищенные, или просто привилегированные) данные или методы в штампах.

Ответ - так же, как это делается в JavaScript в целом. Вот 4 способа.

TL; DR: Closure, Symbol, WeakMap, прокси-объект (через штамп Приватизировать).

1. Закрытие

Это самый известный и самый частный способ скрыть ваши данные. (Прочтите комментарии!)

Давайте создадим BlockchainApi штамп с личными данными token и частной функцией request.

const rp = require('request-promise')
const {init} = require('@stamp/it') // using the init shorthand here
const BlockchainApi = init(function ({config}) {
  // private data
  const token = config.token
  // public property
  this.url = config.url
  // private method accesses both the public and the private data
  const request = ({path = '', body, method = 'GET'} = {}) => {
    return rp({
      url: this.url + '/' + path, // url is public
      headers: {authorization: token}, // but token is private
      json: true,
      body,
      method
    })
  }
  // public method accesses the private method
  this.ping = () => request()
})

В другом файле:

const config = {token: '1', url: 'http://i.ua'}
const blockchainApi = BlockchainApi({config}) // create instance
blockchainApi.ping().then(() => console.log('ok')) // have access

Как видите, штамп BlockchainApi имеет единственный инициализатор. В нем мы создаем общедоступный метод ping, который имеет доступ к закрытому методу request, который, в свою очередь, имеет доступ к закрытым данным token.

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

Плюсы:

  1. Это самый частный способ скрыть данные. Никто не может получить к нему доступ.
  2. Частные методы могут иметь доступ к некоторым общедоступным методам (request имеют доступ к ping).

Минусы:

  1. Неэффективно с памятью. Он создает две закрытые функции для каждого экземпляра объекта.
  2. Чтобы добавить другой метод (например, verifyBlock()), он должен быть частью той же функции init. Вздутие одного штампа - вообще плохая идея. Если штамп BlockchainApi состоит из другого штампа, он не сможет получить доступ к request методу.

2. Символ

Прочтите комментарии.

const rp = require('request-promise')
const stampit = require('@stamp/it')

const requestMethod = Symbol('requestMethod')
const tokenData = Symbol('tokenData')

const BlockchainApi = stampit({
  init ({config}) {
    // protected data
    this[tokenData] = config.token
    // public property
    this.url = config.url
  },

  methods: {
    // protected method accesses both public and protected data
    [requestMethod] ({path = '', body, method = 'GET'} = {}) {
      return rp({
        url: this.url + '/' + path,
        headers: {authorization: this[tokenData]},
        json: true,
        body,
        method
      })
    },

    ping () {
      // public method accesses the protected method
      return this[requestMethod]()
    }
  }
})

В другом файле:

const config = {token: '1', url: 'http://i.ua'}
const blockchainApi = BlockchainApi({config}) // create instance

blockchainApi.ping().then(() => console.log('ok')) // have access

Мы используем символы в качестве ключей свойств. Это означает, что если у кода есть доступ к самому символу, он может получить «защищенные» данные.

Плюсы:

  1. Это полностью доказанный подход к сокрытию данных. Применимо в большинстве случаев.
  2. Удобно для памяти, в отличие от замыканий.
  3. Защищенные методы имеют доступ к всем общедоступным методам.

Минусы:

  1. К личным данным и методу все еще можно получить доступ и изменить с помощью еще некоторого кода JavaScript (пример).
  2. Чтобы добавить другой метод (например, verifyBlock()), он должен быть частью того же файла. Если этот BlockchainApi штамп состоит из другого штампа из другого файла, он не сможет получить доступ к request методу.

3. WeakMap

const rp = require('request-promise')
const stampit = require('@stamp/it')

const mapOfPrivateObjects = new WeakMap()

// private method accesses both the public and private data
function requestMethod ({path = '', body, method = 'GET'} = {}) {
  return rp({
    url: this.url + '/' + path,
    headers: {authorization: mapOfPrivateObjects.get(this).token},
    json: true,
    body,
    method
  })
}

const BlockchainApi = stampit({
  init ({config}) {
    // private data and method
    mapOfPrivateObjects.set(this, {
      token: config.token,
      request: (...args) => requestMethod.apply(this, ...args)
    })
    // public property
    this.url = config.url
  },

  methods: {
    ping () {
      // public method accesses the private method
      return mapOfPrivateObjects.get(this).request()
    }
  }
})

В другом файле:

const config = {token: '1', url: 'http://i.ua'}
const blockchainApi = BlockchainApi({config}) // create instance
blockchainApi.ping().then(() => console.log('ok')) // have access

Мы используем WeakMap в качестве хранилища личных данных для каждого экземпляра объекта. Это лучше, чем символы, теперь данные действительно конфиденциальны. Кроме того, это лучше, чем Closures - более эффективное использование памяти.

Плюсы:

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

Минусы:

  1. Чтобы добавить другой метод (например, verifyBlock()), он должен быть частью того же файла. Если этот BlockchainApi штамп состоит из другого штампа из другого файла, он не сможет получить доступ к request методу.

4. Прокси-объект через штамп Приватизировать

Если вы составите Privatize со своим штампом, то объекты, созданные из вашего нового штампа, будут объектами-оболочками. Они обернут каждый метод (кроме методов, которые вы объявили как частные) в методы-оболочки, которые будут вызывать оригинальные методы экземпляра объекта. См. Картинку.

Возвращенный объект-оболочка будет иметь тот же набор методов, что и исходный экземпляр объекта, но не будет иметь никаких свойств.

const rp = require('request-promise')
const stampit = require('@stamp/it')
const {privatizeMethods} = require('@stamp/privatize')

const BlockchainApi = stampit({
  init({config}) {
    // private data
    this.token = config.token
    // private data too!!! But was public in previous examples
    this.url = config.url
  }
},
privatizeMethods('request'), // Hiding a method
{
  methods: {
    // private method accesses private data, or public methods
    request({path = '', body, method = 'GET'} = {}) {
      return rp({
        url: this.url + '/' + path,
        headers: {authorization: this.token},
        json: true,
        body,
        method
      })
    },

    ping() {
      // public method accesses the private method
      return this.request()
    }
  }
})

В другом файле:

const config = {token: '1', url: 'http://i.ua'}
const blockchainApi = BlockchainApi({config}) // create instance
blockchainApi.ping().then(() => console.log('ok')) // have access

Не уверен, Pro или Con, но все общедоступные методы могут получить доступ ко всем частным методам (например, ключевому слову protected в классических классах). Итак, если у вас есть доступ к экземпляру объекта, созданному из штампа, вы не можете получить доступ к личным данным и методам. Однако вы можете взять исходный штамп, добавить к нему еще один метод и предоставить себе доступ ко всем личным данным и методам.

Плюсы:

  1. Только члены штампа могут получить доступ к личным данным и методам.
  2. Все частные методы могут обращаться ко всем публичным методам.
  3. Вы можете добавить другие методы (например, verifyBlock()) из других файлов. (Если этот штамп BlockchainApi составлен с другим штампом из другого файла, тогда каждый метод будет иметь доступ к методу request.)

Минусы:

  1. Не дружественный к памяти. Создает небольшую функцию-оболочку для каждого метода.
  2. Вся недвижимость становится частной. Вам нужно будет создать геттер для общедоступных свойств. Например. getUrl().

Удачи с марками!