ES6: прокси изнутри

Перевод статьи ES6 Proxies in Depth с сайта ponyfoo.com, опубликовано на css-live.ru с разрешения автора — Николаса Беваквы.

Ура, милости просим. Это «ES6 — Элейн, тебе нужно родить! — изнутри». Что? Никогда о нём не слыхали? Тогда начните с краткой историей инструментария ES6. Затем изучите деструктирование, литералы шаблона, стрелочные функции, оператор расширения и оставшиеся параметры, улучшения в литералах объекта, новый «сахарок» — классы поверх прототипов, let, const и «Временную мёртвую зону», а также итераторы, генераторы, символы и объекты Map, WeakMaps, Sets, и WeakSets. Ну а сегодня поговорим о прокси в ES6.

Как и в прошлых статьях, рекомендую вам установить Babel и повторять за мной, копируя примеры с помощью интерактивной оболочки REPL, либо командной строки babel-node и файла. Это поможет гораздо лучше усвоить идеи, обсуждаемые в серии. Если вы не из тех, кто любит устанавливать что-либо на свой компьютер, то вам есть смысл залезть на CodePen и кликнуть иконку с шестерёнкой для JavaScript — у него есть препроцессор Babel, который с лёгкостью позволяет опробовать ES6. Ещё одна довольно полезная альтернатива, это использовать онлайновый REPL для Babel — он показывает скомпилированный ES5-код справа от ES6-кода, чтобы быстро сравнить.

Заметьте, что Proxy труднее пощупать, поскольку в Babel он заработает лишь при условии, что его поддерживает браузер, в котором его запускают. Можно ознакомиться с таблицей совместимости ES6 для поддерживающих браузеров. Чтобы опробовать Proxy сегодня, воспользуйтесь Microsoft Edge или Mozilla Firefox. Что до меня, то я проверяю свои примеры в Firefox.

Пока мы не начали, позвольте беззастенчиво попросить вашей поддержки, если вы наслаждаетесь моей серией «ES6 изнутри». Ваши взносы пойдут на хостинг сайту, мне на еду, на то, чтоб я не отставал от графика, и на то, чтобы Pony Foo оставался поистине источником джаваскриптовых вкусняшек.

Спасибо, что выслушали, а теперь перейдём к прокси!

Прокси в ES6

Прокси — новая и весьма интересная фича в ES6. Вкратце, Proxy полезен для определения поведения в момент обращения к свойству объекта target. Объект handler может пригодиться для настройки «ловушек» для вашего Proxy, как мы увидим чуть позже.

По умолчанию, от прокси мало толку — по сути они ничего не делают. Если оставить «параметры» пустыми, то прокси будет работать, как «сквозная переадресация» к объекту target — MDN называет это «Ничего не делающий переадресующий прокси», что логично.

var target = {}
var handler = {}
var proxy = new Proxy(target, handler)
proxy.a = 'b'
console.log(target.a)
// <- 'b'
console.log(proxy.c === undefined)
// <- true

Но прокси станет интереснее, если добавить ловушки. С ловушками можно по-разному перехватывать взаимодействия с target , пока эти взаимодействия связаны с прокси. Мы могли бы взять ловушку get, чтобы записывать каждую попытку вытащить значение из свойства в target. Давайте этим и займёмся.

Ловушка get

Следующий прокси может отслеживать все без исключения события обращения к свойству благодаря наличию у него ловушки handler.get. Также можете использовать это для преобразования значения, полученное из обращения к любому свойству. Уже можно представить, как Proxy становится основной фичей в инструментарии разработчика.

var handler = {
  get (target, key) {
    console.info(`Get on property "${key}"`)
    return target[key]
  }
}
var target = {}
var proxy = new Proxy(target, handler)
proxy.a = 'b'
proxy.a
// <- 'Получаем свойство "a"'
proxy.b
// <- 'Получаем свойство "b"'

Конечно, вашему геттеру необязательно возвращать исходное значение target[key]. Как насчёт того, чтобы наконец сделать эти свойства _prop по-настоящему приватными?

set

Знаете, как обычно определяются соглашения типа знака доллара в Angular, где к свойствам с префиксом в виде одного знака доллара вряд ли стоит обращаться из приложения, а к свойствам с префиксом в виде двух знаков доллара нельзя обратиться вообще? Обычно мы сами делаем что-то подобное в приложениях, и как правило в виде переменных с префиксом в виде подчёркивания.

Прокси в примере ниже не позволяет обращаться к свойствам get и set (благодаря ловушке handler.set) во время обращениия к target через proxy. Заметили, что в данном случае set всегда возвращает true? Это значит, что установка свойству key данного значения value должно сработать. Если возвращённое значение для ловушки set равно false, то установка значения свойства в строгом режиме выбросит TypeError, а иначе тихо проигнорируется.

var handler = {
  get (target, key) {
    invariant(key, 'get')
    return target[key]
  },
  set (target, key, value) {
    invariant(key, 'set')
    return true
  }
}
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Недопустимое обращение к ${action} приватного свойства "${key}"`)
  }
}
var target = {}
var proxy = new Proxy(target, handler)
proxy.a = 'b'
console.log(proxy.a)
// <- 'b'
proxy._prop
// <- Ошибка: недопустимое обращение к get приватного свойства "_prop"
proxy._prop = 'c'
// <- Ошибка: недопустимое обращение к set приватного свойства "_prop"

Вы же помните интерполяцию строки при помощи литералов шаблона, не так ли?

Наверное, стоит отметить, что в сценариях с прокси часто бывает нужно сделать объект target (объект, спрятанный за прокси) полностью недоступным извне. Фактически предотвращая прямой доступ к target, заставляя обращаться к нему исключительно через proxy. Клиенты proxy смогут обращаться к target через объект Proxy, но им придётся соблюдать ваши правила доступа — например, «свойства с префиксом _ трогать нельзя».

С этой целью можно просто обернуть скрытый за прокси объект в метод, а затем вернуть прокси.

function proxied () {
  var target = {}
  var handler = {
    get (target, key) {
      invariant(key, 'get')
      return target[key]
    },
    set (target, key, value) {
      invariant(key, 'set')
      return true
    }
  }
  return new Proxy(target, handler)
}
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`Недоступное обращение к ${action} приватного свойства "${key}"`)
  }
}

Применение остаётся прежним, только теперь доступом к target полностью управляют прокси и его коварные ловушки. К этому моменту любые свойства _prop в target полностью доступны через прокси, а поскольку нельзя обратиться к target напрямую извне метода proxied, они навсегда изолированы от клиента.

Вы могли бы возразить, что тот же результат можно получить в ES5, просто используя переменые, приватно доступные только методу proxied, не требуя самого Proxy. Основная разница в том, что прокси позволяет делать доступ к свойству приватным на разных уровнях. Представьте базовый объект underling, содержащий несколько приватных свойств, к которому по-прежнему можно обратиться в каком-нибудь другом модуле middletier, знающем самые сокровенные тайны внутренностей объекта underling. Модуль middletier мог бы возвратить проксированную версию underling, не требуя привязки API к совершенно новому объекту для защиты этих внутренних переменных. Простой блокировки обращения к любым «приватным» свойствам было бы достаточно!

Вот где может пригодиться валидация по схеме с помощью прокси.

Валидация по схеме при помощи прокси

В принципе, можно настроить валидацию по схеме в самом объекте target, и делая это в прокси, вы отделяете проблемы валидации от объекта target, который будет жить долго и счастливо, как самый обычный объект JavaScript. Кроме того, прокси можно использовать в качестве промежуточного звена для обращения к различным объектам, соответствующим схеме, не опираясь на прототипное наследование или классы class в ES6.

В примере ниже, person — обычный объект модели, и также мы определили объект validator с ловушкой set, используемый в качестве обработчика handler для валидатора моделей человека proxy. Пока свойства person устанавливаются через proxy, неизменные части модели будут соответствовать нашим правилам валидации.

var validator = {
  set (target, key, value) {
    if (key === 'age') {
      if (typeof value !== 'number' || Number.isNaN(value)) {
        throw new TypeError('Age должен быть  числом')
      }
      if (value <= 0) {
        throw new TypeError('Age должен быть положительным числом')
      }
    }
    return true
  }
}
var person = { age: 27 }
var proxy = new Proxy(person, validator)
proxy.age = 'foo'
// <- TypeError: Age должен быть  числом
proxy.age = NaN
// <- TypeError: Age должен быть числом
proxy.age = 0
// <- TypeError: Age должен быть положительным числом
proxy.age = 28
console.log(person.age)
// <- 28

Есть ещё особый «строгий» тип прокси, позволяющий при необходимости полностью отключать доступ к target.

Аннулируемые прокси

Аналогично Proxy можно использовать Proxy.revocable. Основные отличия в том, что вернувшимся значением будет { proxy, revoke }, и что после вызова revoke, прокси выбросит ошибку в любой операции. Вернёмся к примеру со сквозным Proxy и сделаем его аннулируемым. Заметьте, что здесь мы не используем оператор new. Многократный вызов revoke() не даст никакого эффекта.

var target = {}
var handler = {}
var {proxy, revoke} = Proxy.revocable(target, handler)
proxy.a = 'b'
console.log(proxy.a)
// <- 'b'
revoke()
revoke()
revoke()
console.log(proxy.a)
// <- TypeError: попытка недопустимой операции с аннулируемым

Этот тип Proxy особенно полезен, поскольку теперь можно полностью отрезать доступ к прокси, предоставляемый клиенту. Вы начинаете с передачи аннулируемого Proxy, держа метод revoke на готове (эй, для этого можете использовать WeakMap), и как только станет ясно, что клиенту больше не нужен доступ к target — даже через proxy — вызывайте .revoke() и отрубайте к черту ему доступ. Давай, до свидания, клиент!

Кроме того, поскольку revoke и ловушки вашего handler живут в одной области видимости, можно устанавливать крайне параноидальные правила типа «Если клиент пытается обратиться к приватному свойству неоднократно, его прокси аннулируются полностью».

Возвращайтесь завтра, и мы обсудим ловушки Proxy помимо get и set.

P.S. Это тоже может быть интересно:

2 Комментарии

  1. faiwer

    На мой взгляд, все примеры в статье оторваны от реальности. Блокировать доступ к приватным поля за счёт тормозящей обёртки совершенно недопустимо. Валидировать модель, выбрасывая ошибки при каждом set-е, чрезвычайно неудобная модель валидации. Срезать доступ к объекту после пары обращений к недопустимым полям — сие должно быть шутка.

    Похоже, что реальная область применения весьма специфична. К примеру на proxy можно переписать KnockoutJS.

    1. Alex

      Скорей всего оно для этого и задумывалось — добавить поддержку «реактивности» для объектов, созданных без применения Object.create().

Оставить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Можно использовать следующие HTML-теги и атрибуты: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

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