ES6: ловушки прокси изнутри
Перевод статьи 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, чтобы познакомиться с темой.
Обработчики ловушек прокси
Особенно интересно в прокси то, как именно их можно использовать для перехвата практически любых взаимодействий с объектом target
, а не только операций get
или set
. Ниже представлен полный список тех ловушек, которые можно настроить.
has
– ловушка для оператораin
deleteProperty
– ловушка для оператораdelete
defineProperty
– ловушка дляObject.defineProperty
и декларативных альтернативenumerate
– ловушка для цикловfor..in
ownKeys
– ловушка дляObject.keys
и связанных с ним методовapply
– ловушка для вызовов функции
Мы пропустим get и set, так как уже рассматривали их вчера; и также здесь не хватает ещё нескольких ловушек, которые мы обсудим в завтрашней статье. Оставайтесь на связи!
has
Чтобы «спрятать» любое свойство, можно использовать handler.has
. Это ловушка для оператора in
. В примере с ловушкой set мы не позволили менять и вообще обращаться к свойствам с префиксом _
, но посторонние скрипты могли по-прежнему «прозвонить» наш прокси, чтобы выяснить, есть там эти свойства или нет. Как в сказке «Три медведя», у нас есть три варианта:
- Можно позволить
key in proxy
провалиться доkey in target
- Можно вернуть
false
(илиtrue
) – независимо от того, есть key или нет. - Можно выбросить ошибку и изначально считать вопрос недопустимым.
Последний вариант крайне суров, и я думаю, что в некоторых ситуациях он оправдан – но нужно будет осознавать, что свойство (или «пространство свойств») на самом деле является защищенным. Зачастую выгоднее просто тихо указать, что свойства в объекте нет (in
дает false). А вариант с «проваливанием», когда вы просто возвращаете результат выражения key in target
, обычно хорош в качестве варианта по умолчанию.
В нашем примере мы, наверное, хотим вернуть false
для свойств в «пространстве свойств» с префиксом в виде подчёркивания и по умолчанию key in target
для всех остальных свойств. Это надежно скроет недоступные свойства от нежелательных посетителей.
var handler = { get (target, key) { invariant(key, 'прочитать') return target[key] }, set (target, key, value) { invariant(key, 'установить') return true }, has (target, key) { if (key[0] === '_') { return false } return key in target } } function invariant (key, action) { if (key[0] === '_') { throw new Error(`Недопустимая попытка ${action} приватное свойство "${key}"`) } }
Заметьте, что обращение к свойствам через прокси теперь возвращает false
при каждом обращении к одному из приватных свойств, и клиент ни за что не догадается, что мы намеренно скрыли от него свойство.
var target = { _prop: 'foo', pony: 'foo' } var proxy = new Proxy(target, handler) console.log('pony' in proxy) // <- true console.log('_prop' in proxy) // <- false console.log('_prop' in target) // <- true
Конечно, вместо этого можно было бы выбросить исключение. Это пригодилось бы, когда попытки обратиться к свойству в приватной области рассматриваются скорее как ошибка, приводящая к нарушению модульности, а не как проблема безопасности в коде, который планируется встраивать в сторонние сайты.
Это и правда зависит от задачи!
deleteproperty
Я пользуюсь оператором delete
довольно часто. Установка свойству undefined
очищает его значение, но это свойство по-прежнему остаётся частью объекта. Использование оператора delete
в свойстве с кодом вроде delete foo.bar
означает, что свойство bar
будет навсегда удалено из объекта foo
.
var foo = { bar: 'baz' } foo.bar = 'baz' console.log('bar' in foo) // <- true delete foo.bar console.log('bar' in foo) // <- false
Помните наш пример с ловушкой set, где мы предотвратили доступ к свойствам с префиксом «_»? В этом коде была проблема. Даже если вы не могли изменить значение _prop
, вы могли удалить свойство целиком с помощью оператора delete
. Даже через объект прокси!
var target = { _prop: 'foo' } var proxy = new Proxy(target, handler) console.log('_prop' in proxy) // <- true delete proxy._prop console.log('_prop' in proxy) // <- false
Можно использовать handler.deleteProperty
чтобы не дать операции delete
сработать. Как и с ловушками get
и set
, выбрасывания в ловушке deleteProperty
вполне хватит для предотвращения удаления свойства.
var handler = { get (target, key) { invariant(key, 'прочитать') return target[key] }, set (target, key, value) { invariant(key, 'установить') return true }, deleteProperty (target, key) { invariant(key, 'удалить') return true } } function invariant (key, action) { if (key[0] === '_') { throw new Error(`Недопустимая попытка ${action} приватное свойство "${key}"`) } }
Если запустить тот же кусок кода, что и раньше, то при попытке удалить _prop
из прокси мы столкнёмся с исключением.
var target = { _prop: 'foo' } var proxy = new Proxy(target, handler) console.log('_prop' in proxy) // <- true delete proxy._prop // <- Error: недопустимая попытка удалить приватное свойство "_prop"
И клиенты, взаимодействующие с target
через proxy
, больше не смогут ничего удалить в нашем пространстве приватных свойств с «_»
defineProperty
Обычно мы используем Object.defineProperty(obj, key, descriptor)
в двух случаях.
- Когда нужна кроссбраузерная поддержка геттеров и сеттеров.
- Каждый раз, когда хотим определить метода доступа для пользовательского свойства.
Свойства, добавленные вручную, предназначены для чтения/записи, удаления и перечисления. А свойства, добавленные через Object.defineProperty
, напротив, по умолчанию предназначены только для чтения, их нельзя перезаписывать, удалять и перечислять — другими словами, изначально свойство полностью неизменяемо. Можно настроить эти аспекты дескриптора свойства, и ниже они перечислены — вместе с их значением по умолчанию при использовании Object.defineProperty
.
configurable: false
отключает большинство изменений в дескрипторе свойства и делает свойство неудаляемымenumerable: false
скрывает свойство от цикловfor..in
иObject.keys
value: undefined
— начальное значение для свойстваwritable: false
делает значение свойства неизменяемымget: undefined
— этот метод ведёт себя, как геттер для свойстваset: undefined
— этот метод получает новое значение и обновляет значение свойства.
Заметьте, что при определении свойства придётся выбрать между использованием value
и writable
или get
и set
. Выбрав первое, вы настраиваете дескриптор данных — такое получается при объявлении свойств вроде foo.bar = 'baz'
, у него есть значение value
и он бывает перезаписываемым (writeable
). При выборе последнего вы создаёте дескриптор метода доступа, полностью определяемый с помощью методов, которые можно использовать, чтобы прочитать (get()
) или задать (set()
) значение свойства.
В примере кода ниже показано, как дескрипторы свойства полностью различаются в зависимости от того, выбрали ли вы декларативный вариант или программный API.
var target = {} target.foo = 'bar' console.log(Object.getOwnPropertyDescriptor(target, 'foo')) // <- { value: 'bar', writable: true, enumerable: true, configurable: true } Object.defineProperty(target, 'baz', { value: 'ponyfoo' }) console.log(Object.getOwnPropertyDescriptor(target, 'baz')) // <- { value: 'ponyfoo', writable: false, enumerable: false, configurable: false }
Теперь, когда мы немного рассмотрели Object.defineProperty
, настало время ловушки.
Ловушка
Ловушка handler.defineProperty
нужна для перехвата вызовов Object.defineProperty
. Используются key
и descriptor
. Пример ниже полностью запрещает добавлять свойства через прокси. Ну не здорово ли, что это перехватывает и декларативный вариант объявления свойства foo.bar = 'baz'
? Еще как здорово!
var handler = { defineProperty (target, key, descriptor) { return false } } var target = {} var proxy = new Proxy(target, handler) proxy.foo = 'bar' // <- TypeError: обработчик defineProperty прокси вернул false для свойства '"foo"'
В примере с «приватными свойствами» мы могли бы использовать ловушку defineProperty
для предотвращения создания приватных свойств через прокси. Мы заново воспользуемся методом, который у нас уже был, чтобы выбросить ошибку для определения свойства в «приватном пространстве с префиксом _
», вот и всё.
var handler = { defineProperty (target, key, descriptor) { invariant(key, 'определить') return true } } function invariant (key, action) { if (key[0] === '_') { throw new Error(`Недопустимая попытка ${action} приватное свойство "${key}"`) } }
Затем можно проверить его на объекте target
, установка свойства с префиксом _ теперь выбросит ошибку. Можно сделать, чтобы это тихо проигнорировалось, вернув false
— зависит от вашей задачи.
var target = {} var proxy = new Proxy(target, handler) proxy._foo = 'bar' // <- Error: недопустимая попытка определить свойство "_foo"
Теперь прокси надёжно скрывает приватные свойства с префиксом _
за ловушкой, которая не дает установить их ни через proxy[key] = value
, ни через Object.defineProperty(proxy, key, { value })
— потрясающе!
Метод enumerate
Метод handler.enumerate
можно использовть для перехвата оператора for..in
. C has
можно было не дать key in proxy
вернуть true
для всех наших приватных свойств с подчеркиванием, но что насчёт цикла for..in
? Даже при том, что ловушка has
скрывает свойство от проверки key in proxy
, клиент случайно наткнётся на свойство при использовании цикла for..in
!
var handler = { has (target, key) { if (key[0] === '_') { return false } return key in target } } var target = { _prop: 'foo' } var proxy = new Proxy(target, handler) for (let key in proxy) { console.log(key) // <- '_prop' }
С помощью ловушки enumerate
можно вернуть итератор, который заменит перечисляемые свойства, найденные в прокси в ходе цикла for..in
. Полученный итератор должен соответствовать протоколу «Итератор», например, итераторы, полученные из любого метода Symbol.iterator
. Вот возможная реализация такого прокси, который возвращает вывод Object.keys
за вычетом свойств из приватной области.
var handler = { has (target, key) { if (key[0] === '_') { return false } return key in target }, enumerate (target) { return Object.keys(target).filter(key => key[0] !== '_')[Symbol.iterator]() } } var target = { pony: 'foo', _bar: 'baz', _prop: 'foo' } var proxy = new Proxy(target, handler) for (let key in proxy) { console.log(key) // <- 'pony' }
Теперь даже пытливый for...in
ни за что не увидит наших приватных свойств!
ownKeys
С помощью метода handler.ownKeys
можно вернуть массив Array
со свойствами, который станет результатом для Reflect.ownKeys()
— он должен включать все свойства target
(перечисляемые или нет, а также символы). Реализация по умолчанию, как показано ниже, может просто вызвать метод Reflect.ownKeys
у проксированного объекта target
. Не волнуйтесь, через пару статей мы доберемся до встроенного объекта Reflect
.
var handler = { ownKeys (target) { return Reflect.ownKeys(target) } }
В этом случае перехват не повлиял бы на вывод Object.keys
.
var target = { _bar: 'foo', _prop: 'bar', [Symbol('secret')]: 'baz', pony: 'ponyfoo' } var proxy = new Proxy(target, handler) for (let key of Object.keys(proxy)) { console.log(key) // <- '_bar' // <- '_prop' // <- 'pony' }
Заметьте, что перехватчик ownKeys
используется во время всех следующих операций.
Object.getOwnPropertyNames()
— возвращает только не-символьные свойстваObject.getOwnPropertySymbols()
– возвращает только символьные свойстваObject.keys()
– возвращает только не-символьные перечисляемые свойстваReflect.ownKeys()
— мы вернёмся кReflect
через пару статей!
В случае, когда нужно отключить доступ к пространству свойств с префиксом _
, можно взять вывод Reflect.ownKeys(target)
и отфильтровать его.
var handler = { ownKeys (target) { return Reflect.ownKeys(target).filter(key => key[0] !== '_') } }
Если теперь воспользоваться обработчиком в коде выше, чтобы вытащить ключи объекта, мы обнаружим лишь свойства в публичном пространстве, а не в пространстве с префиксом _
.
var target = { _bar: 'foo', _prop: 'bar', [Symbol('secret')]: 'baz', pony: 'ponyfoo' } var proxy = new Proxy(target, handler) for (let key of Object.keys(proxy)) { console.log(key) // <- 'pony' }
Это не повлияло бы на перебор Symbol
, поскольку sym[0]
возвращает undefined
— и во всяком случае уж точно не '_'
..
var target = { _bar: 'foo', _prop: 'bar', [Symbol('secret')]: 'baz', pony: 'ponyfoo' } var proxy = new Proxy(target, handler) for (let key of Object.getOwnPropertySymbols(proxy)) { console.log(key) // <- Symbol(secret) }
Нам удалось скрыть свойства с префиксом _
от перечисления ключей, оставив символы и другие свойства без изменений.
apply
Метод handler.apply
весьма интересен. Его можно использовать в качестве ловушки для любого вызова proxy
. Каждый пример в коде ниже пройдёт через ловушку apply
для вашего прокси.
proxy(1, 2) proxy(...args) proxy.call(null, 1, 2) proxy.apply(null, [1, 2])
Метод apply
принимает три аргумента.
target
— функция, скрываемая за проксиctx
— контекст, переданный какthis
вtarget
при применении вызоваargs
— аргументы, переданные какthis
вtarget
при применении вызова
Можно попытаться реализовать это «в лоб» в виде target.apply(ctx, args)
, но ниже мы будем использовать Reflect.apply(...arguments)
. Мы познакомимся со встроенным объектом Reflect
через пару статей. А пока думайте о них, как об эквивалентных, и учитывайте, что значение, которое вернула ловушка apply
, также будет использоваться в качестве результата вызова функции через proxy
.
var handler = { apply (target, ctx, args) { return Reflect.apply(...arguments) } }
Помимо очевидной «возможности записывать все параметры каждого вызова функции для прокси», эту ловушку можно использовать для жонглирования параметрами и чтобы настраивать результаты вызова функций, не изменяя сам метод — и также не изменяя вызывающий код.
В примере ниже проксируется метод sum
с помощью ловушки twice
, удваивающей результат sum
, не меняя ничего в коде, разве что вместо вызова метода sum напрямую теперь используется proxy
.
var twice = { apply (target, ctx, args) { return Reflect.apply(...arguments) * 2 } } function sum (left, right) { return left + right } var proxy = new Proxy(sum, twice) console.log(proxy(1, 2)) // <- 6 console.log(proxy(...[3, 4])) // <- 14 console.log(proxy.call(null, 5, 6)) // <- 22 console.log(proxy.apply(null, [7, 8])) // <- 30
Конечно, ловушка apply
также поймает и вызов Reflect.apply
для прокси.
Reflect.apply(proxy, null, [9, 10]) // <- 38
А где ещё может пригодиться
handler.apply
?
Завтра я опубликую последнюю статью про Proxy
— обещаю!. В ней я расскажу про оставшиеся ловушечные обработчики, к примеру, construct
и getPrototypeOf
. Чтобы не пропустить их, подписывайтесь ниже.
Прим. перев.: со времени публикации метод handler.enumerate
устарел, см. подробности на MDN.
P.S. Это тоже может быть интересно:
handler.enumerate() больше нет, его убрали.
Спасибо за замечание. Дописал об этом в конце статьи!