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. Это тоже может быть интересно:
None Found
На мой взгляд, все примеры в статье оторваны от реальности. Блокировать доступ к приватным поля за счёт тормозящей обёртки совершенно недопустимо. Валидировать модель, выбрасывая ошибки при каждом set-е, чрезвычайно неудобная модель валидации. Срезать доступ к объекту после пары обращений к недопустимым полям — сие должно быть шутка.
Похоже, что реальная область применения весьма специфична. К примеру на proxy можно переписать KnockoutJS.
Скорей всего оно для этого и задумывалось — добавить поддержку «реактивности» для объектов, созданных без применения Object.create().