ES6: ещё о ловушках прокси изнутри
Перевод статьи More ES6 Proxy Traps in Depth с сайта ponyfoo.com, опубликовано на css-live.ru с разрешения автора — Николаса Беваквы.
Привет! Это «ES6 — Ловушки? Снова? — изнутри». Интересуетесь и другими вкусняшками 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 оставался поистине источником джаваскриптовых вкусняшек.
Спасибо, что выслушали, а теперь перейдём к остальным ловушкам Proxy! Если не понимаете, о чём речь, советую прочитать предыдущие статьи про встроенный объект Proxy и про первые несколько ловушек прокси, о которых я рассказывал вчера.
Постойте! Есть ещё… (Обработчики ловушек прокси)
В этой статье рассматриваются ловушечные обработчики, не вошедшие в две предыдущие статьи про прокси. В целом, рассмотренные вчера ловушки, были связаны с манипуляцией свойствами, тогда как первые пять ловушек, с которыми мы познакомимся сегодня, касаются самого объекта, скрываемого за прокси. Последние две ловушки также касаются свойств — но они немного сложнее вчерашних, в которые было гораздо легче «попасться» (в ловушку! — йо-хо-хо) в повседневном коде.
construct
– ловушка для использования оператора newgetPrototypeOf
– ловушка для внутренних вызовов[[GetPrototypeOf]]
setPrototypeOf
– ловушка для вызововObject.setPrototypeOf
isExtensible
– ловушка для вызововObject.isExtensible
preventExtensions
– ловушка для вызововObject.preventExtensions
getOwnPropertyDescriptor
– ловушка для вызововObject.getOwnPropertyDescriptor
construct
Метод handler.construct
можно использовать, чтобы поймать использование оператора new
. Вот быстрая «реализация по умолчанию», нисколько не меняющая поведение new
. Не забыли, что у нас есть новый приятель — оператор расширения?
var handler = { construct (target, args) { return new target(...args) } }
Если написать handler
, как выше, то поведение new, к которому вы уже привыкли, не изменится. Это здорово, поскольку что бы вы ни пытались получить, всегда можно вернуться к поведению по умолчанию — что всегда важно.
function target (a, b, c) { this.a = a this.b = b this.c = c } var proxy = new Proxy(target, handler) console.log(new proxy(1,2,3)) // <- { a: 1, b: 2, c: 3 }
Очевидные применения для ловушек construct
: подготовка данных в аргументах, действия, обязательные при каждом вызове new proxy()
, регистрация и отслеживание создания объекта и замена реализаций целиком. Представьте себе прокси в тех ситуациях, где есть разные «ветки» наследования.
class Automobile {} class Car extends Automobile {} class SurveillanceVan extends Automobile {} class SUV extends Automobile {} class SportsCar extends Car {} function target () {} var handler = { construct (target, args) { var [status] = args if (status === 'nsa') { return new SurveillanceVan(...args) } if (status === 'single') { return new SportsCar(...args) } return new SUV(...args) // семья } }
Конечно, само ветвление можно сделать и обычными методами, но оператор new
также разумно использовать, если в конечном итоге вы всё равно создаёте новый объект во всех ветках кода.
console.log(new proxy('nsa').constructor.name) // <- `SurveillanceVan`
Но всё же чаще всего ловушки construct
понадобятся для чего-то попроще: для расширения объекта target
после создания — и прежде любых других действий — так, чтобы он поддерживал прокси-фильтр. Возможно, к объекту target
придётся добавить флаг proxied
или нечто похожее.
getPrototypeOf
Вот для чего может пригодиться метод handler.getPrototypeOf
в качестве ловушки:
- Для свойства
Object.prototype.__proto__
- Для метода
Object.prototype.isPrototypeOf()
- Для метода
Object.getPrototypeOf()
- Для метода
Reflect.getPrototypeOf()
- Для оператора
instanceof
Эту ловушку можно использовать, чтобы заставить объект притворяться, что он Array
при обращении через прокси. Но учитывайте, что в этом случае сам proxy
не становится настоящим Array
var handler = { getPrototypeOf: target => Array.prototype } var target = {} var proxy = new Proxy(target, handler) console.log(proxy instanceof Array) // <- true console.log(proxy.push) // <- undefined
Конечно, можно продолжать патчить прокси до нужного результата. В этом случае можно воспользоваться ловушкой get
, чтобы «смешать» Array.prototype
с фактическим конечным target
. Всякий раз, когда свойство в target
не найдено, нам потребуются reflection-методы, чтобы найти его в Array.prototype
. Оказывается этого вполне достаточно для большинства операций.
var handler = { getPrototypeOf: target => Array.prototype, get (target, key) { return Reflect.get(target, key) || Reflect.get(Array.prototype, key) } } var target = {} var proxy = new Proxy(target, handler) console.log(proxy.push) // <- function push () { [нативный код] } proxy.push('a', 'b') console.log(proxy) // <- { 0: 'a', 1: 'b', длина: 2 }
Я определённо вижу продвинутые применения для ловушек getPrototypeOf
в будущем, но пока слишком рано говорить, какие паттерны могут из этого выйти.
setPrototypeOf
Название метода Object.setPrototypeOf
говорит само за себя: он присваивает прототипу объекта ссылку на другой объект. Считается правильным устанавливать прототипы именно так, а не использовать __proto__
, который уже устарел — что теперь закреплено и в стандарте.
Метод handler.setPrototypeOf
можно использовать, чтобы установить ловушку для Object.setPrototypeOf
. Код ниже не меняет поведению по умолчанию изменения прототипа на значение proto
.
var handler = { setPrototypeOf (target, proto) { Object.setPrototypeOf(target, proto) } } var proto = {} var target = function () {} var proxy = new Proxy(target, handler) proxy.setPrototypeOf(proxy, proto) console.log(proxy.prototype === proto) // <- true
Поле действия для setPrototypeOf
широкое. Можно просто не вызывать Object.setPrototypeOf
и ловушка превратила бы вызов в холостой. А можно выбросить исключение, делая отказ более явным — например, если полагаете, что новый прототип будет недопустимым, или не хотите, чтобы клиенты неожиданно подложили вам свинью.
Эта ловушка полезна, если нужно ограничить прокси возможности что-то делать с объектом target
. Уж я бы точно подстраховал на всякий случай прокси, отдаваемый стороннему коду, ловушкой наподобие такой:
var handler = { setPrototypeOf (target, proto) { throw new Error('Изменение прототипа запрещено') } } var proto = {} var target = function () {} var proxy = new Proxy(target, handler) proxy.setPrototypeOf(proxy, proto) // <- Ошибка: Изменение прототипа запрещено
С другой стороны, при желании запутать клиентов, можно вообще превратить вызов в холостой — и это, возможно, заставит их уйти.
isExtensible
Метод handler.isExtensible
в основном нужен для регистрации или проверки вызовов Object.isExtensible
. На эту ловушку наложено суровое непременное условие, жестко ограничивающее возможности что-то с ней делать.
Если
Object.isExtensible(proxy) !== Object.isExtensible(target)
, то будет выброшенTypeError
.
Если не хотите сообщать клиенту, расширяемый ли оригинальный объект или нет, можно выбросить ошибку в ловушке handler.isExtensible
, но кажется, есть редкие случаи, когда такой кошмарный ужас оправдан. Для наглядности, ловушка isExtensible
в коде ниже иногда выбрасывает ошибки, но в остальных случаях ведёт себя как ожидалось.
var handler = { isExtensible (target) { if (Math.random() > 0.1) { throw new Error('вам придётся полюбить случайные непонятные ошибки!') } return Object.isExtensible(target) } } var target = {} var proxy = new Proxy(target, handler) console.log(Object.isExtensible(proxy)) // <- true console.log(Object.isExtensible(proxy)) // <- true console.log(Object.isExtensible(proxy)) // <- true console.log(Object.isExtensible(proxy)) // <- Ошибка: вам придётся полюбить случайные непонятные ошибки!
Хотя от этой ловушки мало толку, кроме как для проверки и перестраховки, в надёжном ограничении есть смысл, поскольку есть ещё ловушка preventExtensions
. Она немного полезнее!
preventExtensions
Можно использовать handler.preventExtensions
, чтобы поймать метод Object.preventExtensions
. Если запретить расширения в объекте, то будет нельзя добавлять новые свойства — объект не сможет расширяться.
Представьте сценарий, в которых нужна возможность запретить расширение с помощью preventExtensions
на некоторых объектах — но не на всех. В таком сценарии можно использовать WeakSet
, чтобы следить за объектами, предназначенными для расширения. Если объект есть в наборе, тогда ловушка должна уметь захватывать эти запросы и аннулировать их. В коде ниже именно это и происходит. Заметьте, что ловушка всегда возвращает противоположность Object.isExtensible(target)
, поскольку должна сообщить, сделан ли объект расширяемым или нет.
var mustExtend = new WeakSet() var handler = { preventExtensions (target) { if (!mustExtend.has(target)) { Object.preventExtensions(target) } return !Object.isExtensible(target) } }
Теперь, когда handler
и WeakSet
у нас готовы, пора создать конечный объект, прокси, добавив конечный в наш набор. Затем воспользуемся Object.preventExtensions
и заметим, что он не отменяет расширения в объекте. Так задумано, поскольку target
можно найти в _наборе_ mustExtend
.
var target = {} var proxy = new Proxy(target, handler) mustExtend.add(target) Object.preventExtensions(proxy) // <- TypeError: обработчик preventExtensions прокси вернул false
Если удалить target из набора mustExtend
до вызова Object.preventExtensions
, то target будет сделан нерасширяемым, как и планировалось.
var target = {} var proxy = new Proxy(target, handler) mustExtend.add(target) mustExtend.delete(target) Object.preventExtensions(proxy) console.log(Object.isExtensible(proxy)) // <- false
Конечно, можно воспользоваться этим различием и не дать клиентам прокси сделать его нерасширяемым в случаях, где это может привести к нежелательному поведению. Хотя в большинстве случаях, эта ловушка вам вряд ли пригодится. Потому что обычно вы работаете с конечным target
, а не с самим объектом прокси.
getOwnPropertyDescriptor
Метод handler.getOwnPropertyDescriptor
можно использовать в качестве ловушки для Object.getOwnPropertyDescriptor
. Он может вернуть дескриптор свойства, например, результат Object.getOwnPropertyDescriptor(target, key);
или undefined
, сообщая, что свойство не существует. И как обычно, есть также третий вариант: выбросить исключение, полностью прервав операцию.
Возвращаясь к нашему стандартному примеру с «приватным пространством свойств», можно реализовать ловушку ниже, чтобы не дать клиентам узнать о дескрипторах свойств приватных свойств.
var handler = { getOwnPropertyDescriptor (target, key) { invariant(key, 'получить дескриптор свойства для') return Object.getOwnPropertyDescriptor(target, key) } } function invariant (key, action) { if (key[0] === '_') { throw new Error(`Недопустимое обращение к ${action} приватного свойства "${key}"`) } } var target = {} var proxy = new Proxy(target, handler) Object.getOwnPropertyDescriptor(proxy, '_foo') // <- Ошибка: недопустимая попытка получить дескриптор свойства для приватного свойства "_foo"
Проблема такого подхода в том, что вы фактически сообщаете клиентам, что свойства с префиксом _
так или иначе запрещены. Возможно, было бы правильнее скрыть их полностью, вернув undefined
. Таким образом, поведение приватных свойств будет таким же, как и у свойств, которые на самом деле отсутствуют в объекте target
.
var handler = { getOwnPropertyDescriptor (target, key) { if (key[0] === '_') { return } return Object.getOwnPropertyDescriptor(target, key) } } var target = { _foo: 'bar', baz: 'tar' } var proxy = new Proxy(target, handler) console.log(Object.getOwnPropertyDescriptor(proxy, 'wat')) // <- undefined console.log(Object.getOwnPropertyDescriptor(proxy, '_foo')) // <- undefined console.log(Object.getOwnPropertyDescriptor(proxy, 'baz')) // <- { value: 'tar', writable: true, enumerable: true, configurable: true }
Обычно, при попытке скрыть что-то, лучше заставить его вести себя так, как будто это что-то из совсем другой категории. Но если выскочит ошибка, это послужит сообщением, что «здесь есть что-то сомнительное, но мы не знаем, что именно…» — и в итоге клиент выяснит, почему это так.
Учтите, что если отладочные соображения перевешивают соображения безопасности, вероятно, лучше выбрать оператор
throw
.
Заключение
Это определённо было весело. Теперь я гораздо лучше понимаю прокси, и считаю, что они моментально станут популярными, как только ES6 начнёт набирать обороты. И я безумно рад, что скоро они будут поддерживаться в большинстве браузеров. Но я бы не стал сильно надеяться на прокси для Babel, поскольку многие из ловушек до нелепого трудно (а то и вообще невозможно) реализовать в ES5.
Насколько мы поняли за последние дни, для прокси есть множество применений. Навскидку:
- Можно добавлять правила валидации — и следить за их выполнением — на простых старых объектах JavaScript.
- Можно следить за каждым взаимодействием, происходящем через прокси
- Можно декорировать объекты, не изменяя их вообще.
- Можно делать определённые свойства в объекте полностью невидимыми для клиентов прокси.
- Можно по своей воле отменять доступ, когда клиент больше не должен использовать прокси.
- Можно изменять аргументы, переданные в проксированный метод
- Можно изменять результат, произведённый проксированным методом
- Можно предотвращать удаление конкретных свойств через прокси
- Можно не давать определить новые свойства, согласно требуемому дескриптору свойства
- Можно перемешивать аргументы в конструкторе
- Можно возвращать что-то помимо объекта, только что созданного в конструкторе оператором new.
- Можно заменять прототип объекта на что-нибудь ещё.
Не сомневаюсь, что для прокси есть ещё сотни применений. И уверен, что многие библиотеки примут паттерн, который мы обсуждали в этой серии, когда «конечный» объект target создаётся и используется с целью хранения, но клиенту предоставляется только «передний край» в виде объекта прокси с ограниченным и проверяемым взаимодействием с конечным.
А для чего бы использовали
Proxy
вы?
Встретимся завтра… скажем, в это же время? Можно будет поговорить про встроенный объект Reflect
.
P.S. Это тоже может быть интересно: