Как использовать Proxy в JavaScript?
год назад·6 мин. на чтение
Раскрываем возможности прокси (Proxy) JavaScript, погружаемся в кастомизацию операций с объектами
Введение
В мире JavaScript разработчики постоянно ищут способы повысить мощность и гибкость своего кода. Прокси в JavaScript, представленный в ECMAScript 6 (ES6), стал замечательной фичей, позволяющей разработчикам перехватывать и настраивать основные операции над объектами. Выступая в качестве посредника, прокси открывают множество возможностей, позволяя разработчикам с легкостью изменять поведение объектов, проверять входные данные и защищать их. В этой статье мы углубимся во внутреннюю работу JavaScript Proxy. Мы рассмотрим его фундаментальное использование, концепцию ловушек, определяющих поведение прокси, и различные типы доступных ловушек. Кроме того, мы узнаем, как прокси можно использовать для валидации значений, проверки безопасности и защиты данных, а также для устранения ограничений, совместимости браузеров и соображений производительности. Итак, давайте отправимся в это путешествие открытий и разгадаем секреты JavaScript Proxy!Базовое использование прокси JavaScript
При работе с JavaScript Proxy первым шагом является создание прокси-объекта с использованием нового синтаксисаnew Proxy()
Этот синтаксис позволяет определить целевой объект и объект обработчика, содержащий ловушки. Эти ловушки представляют собой специальные методы, которые перехватывают операции на прокси и позволяют нам настраивать их поведение.
Например, get
перехватывает доступ к свойству прокси-объекта. Определив эту ловушку, мы можем настроить реакцию прокси при доступе к свойству. Точно так же set
перехватывает присвоение свойства, позволяя нам проверить или изменить присвоенное значение.
Таким образом, ловушки в контексте прокси JavaScript — это специальные методы, которые действуют как посредники, перехватывая и позволяя настраивать операции объекта, такие как доступ к свойствам и назначение прокси-объекта. Они предоставляют разработчикам детальный контроль над поведением прокси, позволяя настраивать и манипулировать по мере необходимости.
В приведенном выше примере мы создаем прокси-объект, который является оболочкойconst target = { name: 'John', age: 20 }; const handler = { get(target, property) { console.log(`Getting property: ${property}`); return target[property]; }, set(target, property, value) { console.log(`Setting property: ${property} = ${value}`); target[property] = value; } }; const proxy = new Proxy(target, handler); console.log(proxy.name); // Output: Getting property: name, John proxy.age = 30; // Output: Setting property: age = 20 console.log(proxy.age); // Output: Getting property: age, 30
target
объекта. get
логирует доступное свойство и возвращает соответствующее значение из target
объекта. Аналогичным образом, set
логирует заданное свойство и присваивает значение target
объекту.
Используя эти ловушки, мы можем беспрепятственно перехватывать и изменять операции с объектами в соответствии с нашими требованиями.
Общие сведения о прокси-ловушках
Прокси JavaScript предоставляет несколько ловушек, каждая из которых соответствует отдельной операции с прокси-объектом. Некоторые из часто используемых ловушек включаютget
, set
, has
, apply
, construct
и другие. Эти ловушки позволяют нам перехватывать и настраивать такие операции, как доступ к свойствам, назначение, вызов функций, создание экземпляров объектов и многое другое.
Давайте рассмотрим некоторые из этих ловушек на примерах:
get
В этом примереconst target = { name: 'John', age: 20 }; const handler = { get(target, property) { console.log(`Getting property: ${property}`); return target[property]; } }; const proxy = new Proxy(target, handler); console.log(proxy.name); // Output: Getting property: name, John console.log(proxy.age); // Output: Getting property: age, 20
get
перехватывает доступ к свойству на прокси и логирует доступ к свойству перед возвратом его значения.
set
Здесьconst target = { name: 'John', age: 20 }; const handler = { set(target, property, value) { console.log(`Setting property: ${property} = ${value}`); target[property] = value; } }; const proxy = new Proxy(target, handler); proxy.name = 'Jerry'; // Output: Setting property: name = Jerry console.log(proxy.name); // Output: Jerry
set
перехватывает назначение свойства на прокси и логирует назначенное свойство и значение, прежде чем изменять их в целевом объекте.
Используя эти и другие ловушки, мы можем эффективно настраивать и контролировать поведение прокси-объектов в соответствии с нашими конкретными потребностями.
Перехват операций с объектами с помощью прокси
JavaScript Proxy позволяет нам перехватывать и настраивать различные операции с объектами. Давайте рассмотрим несколько примеров:Удаление свойства
В этом примере ловушкаconst target = { name: 'John', age: 20 }; const handler = { deleteProperty(target, property) { console.log(`Deleting property: ${property}`); delete target[property]; } }; const proxy = new Proxy(target, handler); delete proxy.age; // Output: Deleting property: age console.log(proxy); // Output: { name: 'John' }
deleteProperty
перехватывает удаление свойства прокси-объекта. Мы можем настроить поведение, чтобы логировать удаленное свойство, а затем удалить его из целевого объекта.
Перехват вызова функции
В этом случае ловушкаconst target = { sum: (a, b) => a + b }; const handler = { apply(target, thisArg, argumentsList) { console.log(`Calling function: ${target.name}`); return target.apply(thisArg, argumentsList); } }; const proxy = new Proxy(target.sum, handler); console.log(proxy(2, 3)); // Output: Calling function: sum, 5
apply
перехватывает вызов функции в прокси-объекте. Мы можем настроить поведение для логирования имени функции, прежде чем вызывать ее с предоставленными аргументами.
Перехват проверки наличия свойства
Ловушкаhas
срабатывает, когда оператор in
используется для проверки наличия свойства в прокси-объекте. Определив эту ловушку, можно настроить реакцию прокси-объекта на проверки существования свойства. Это полезно, если требуется динамически управлять наличием свойств или реализовать механизмы управления доступом.
В приведенном примере кода мы создаем объектconst target = { name: 'John', age: 20 }; const handler = { has(target, property) { console.log(`Checking property existence for "${property}"`); return property in target; } }; const proxy = new Proxy(target, handler); console.log('name' in proxy); // Output: Checking property existence for "name", true console.log('email' in proxy); // Output: Checking property existence for "email", false
handler
с ловушкой has
. Внутри ловушки has
мы логируем сообщение, указывающее, что выполняется проверка существования свойства. Затем мы используем оператор in
, чтобы проверить, существует ли свойство в прокси-объекте. Ловушка перехватывает эту операцию и возвращает true
или false
в зависимости от того, существует ли свойство в базовом целевом объекте.
Перехват создания объекта
Ловушкаconstruct
в JavaScript Proxy перехватывает создание объекта, когда прокси-объект используется в качестве функции-конструктора с new
. Он позволяет настраивать процесс создания экземпляров, проверять аргументы конструктора и применять определенные шаблоны создания объектов.
В примере кода мы определяем объектclass Person { constructor(name) { this.name = name; } } const handler = { construct(target, argumentsList) { console.log(`Creating new instance of "${target.name}" with arguments: ${argumentsList}`); return new target(...argumentsList); } }; const ProxyPerson = new Proxy(Person, handler); const john = new ProxyPerson('John'); // Output: Creating new instance of "Person" with arguments: John
handler
с помощью construct
. Когда прокси-объект вызывается с new
, construct
перехватывает создание объекта. Мы логируем сообщение, указывающее на создание нового экземпляра с конкретными аргументами. Ловушку construct
можно использовать для реализации пользовательской логики инициализации, проверки входных данных или обеспечения требуемого поведения во время создания объекта.
Прокси для валидации и безопасности
Одним из мощных вариантов использования JavaScript Proxy является проверка данных и безопасность. Прокси могут использоваться для обеспечения соблюдения определенных правил и ограничений на доступ к данным и их изменение. Например, мы можем гарантировать, что доступ к определенным свойствам возможен только при определенных условиях, или ограничить несанкционированные изменения. Рассмотрим следующий пример:В этом примереconst target = { password: 'secretpassword' }; const handler = { get(target, property) { if (property === 'password') { console.log('Access denied!'); return undefined; } return target[property]; }, set(target, property, value) { if (property === 'password') { console.log('Unauthorized modification!'); return false; } target[property] = value; return true; } }; const proxy = new Proxy(target, handler); console.log(proxy.username); // Output: undefined console.log(proxy.password); // Output: Access denied! proxy.password = 'newpassword'; // Output: Unauthorized modification!
get
ограничивает доступ к свойству password
, запрещая его извлечение. Аналогичным образом, ловушка set
предотвращает несанкционированное изменение свойства password
.
Используя JavaScript Proxy, мы можем добавить дополнительный уровень проверки и безопасности к нашим данным, гарантируя, что выполняются только разрешенные операции.
Ограничения и совместимость с браузерами
Хотя прокси JavaScript является мощной функцией, у него есть некоторые ограничения, о которых следует помнить. Во-первых, не все браузеры поддерживают прокси, поэтому важно проверить совместимость браузеров, прежде чем использовать их в продакшене. Однако с ростом внедрения ECMAScript 6 поддержка прокси стала более распространенной. Еще одно ограничение заключается в том, что прокси не могут перехватывать определенные операции, которые считаются фундаментальными или внутренними для JavaScript. К ним относятся такие операции, какObject.preventExtensions()
, Object.isExtensible()
и Object.setPrototypeOf()
. Поэтому прокси могут подходить не для всех сценариев и вариантов использования.
Кроме того, стоит отметить, что использование прокси может привести к снижению производительности, особенно при работе с крупномасштабными приложениями. Перехват и настройка операций с объектами сопряжены с затратами. Хотя прокси могут быть оптимизированы в определенных случаях, важно учитывать потенциальное влияние на производительность и оценивать, перевешивают ли преимущества компромиссы с производительностью.
Реальные варианты использования прокси в JavaScript
JavaScript Proxy находит практическое применение в различных реальных сценариях. Вот несколько примеров:- Логирование: прокси можно использовать для логирования операций с объектами и отслеживания изменений, предоставляя ценную отладочную информацию.
- Кэширование: прокси могут реализовывать механизмы кэширования для повышения производительности за счет хранения и извлечения вычисленных или дорогостоящих значений.
- Мемоизация: прокси можно использовать для реализации мемоизации, которая помогает оптимизировать вызовы функций, кэшируя их результаты на основе предоставленных аргументов.
- Контроль доступа: прокси могут применять правила контроля доступа, гарантируя, что доступ к определенным свойствам или операциям могут получить только авторизованные сущности.
Итоги
JavaScript Proxy предлагает разработчикам мощный инструмент для перехвата и настройки операций с объектами в JavaScript. Используя концепцию ловушек, прокси обеспечивают детальный контроль над фундаментальными операциями, позволяя гибко и динамично изменять поведение. В этой статье мы рассмотрели основы использования прокси JavaScript, поняли, как определять ловушки. Мы также выяснили, как прокси можно использовать для проверки, безопасности, а также изучили их ограничения производительности. Вооружившись этими знаниями, теперь вы можете уверенно использовать JavaScript Proxy для создания надежных и безопасных приложений, используя его потенциал для настройки и контроля.Что такое функтор? Функциональное программирование
2 года назад·5 мин. на чтение
В этой статье на простых и доступных примерах рассмотрим одну из концепций функционального программирования - Функтор.
Это серия статей о функциональном программировании:
Функтор
Как мы обсуждали ранее, нам нужна только обертка, которая абстрагируется от обработки данных. Итак, роль функтора Имплементация функтора
Как использовать функтор
С валидными данными:
Функтор Решение задачи с функтором
Комментарии к решению с функтором
Мы смогли улучшить традиционное решение при помощи функтора
- Парадигмы программирования
- Композиция
- Функторы (рассматривается в этой статье)
- Каррирование
- Чистые функции
- Функции первого класса
Что такое функтор?
Функтор (functor) это:- обертка над значением,
- предоставляет интерфейс для преобразование (map),
- подчиняется законам функтора (поговорим о них позже).
Примеры функторов
- Массив (
Array
), - Промис (
Promise
).
Почему массив - функтор?
Вспомним определение функтора:- обертка над списком значений,
- предоставляет интерфейс для преобразования - метод
map
, - подчиняется законам функтора.
[1, 2, 3] // обернутое значение .map( // интерфейс для преобразования значения x => x * 2 )
Почему промис - функтор?
Промис это:- обертка над любым значением из JavaSctipt типов,
- предоставляет интерфейс для преобразования - метод
then
, - подчиняется законам функтора.
const promise = new Promise((resolve, reject) => { resolve( { data: "value" } // обернутое значение, в данном случае объект ) }); promise .then( // интерфейс для преобразования значения response => console.log(response) );
Что объединяет массив (или промис) и функтор?
Функтор - это паттерн проектирования, аArray
и Promise
- типы данных, которые основаны на этом паттерне.
Почему мы говорим, что массив и промис - функторы?
Чтобы понять, что функторы ближе чем кажутся. Массив и промис легко понять, при это они являются мощной концепцией. Мы используем их ежедневно, даже не подозревая об их сущности.Где использовать функторы?
Немного поговорив о функторах и связав их с нашим повседневным использованием, было бы разумно рассмотреть их подробнее. Чтобы лучше понять идею функтора, создадим свои собственные функторы. Для начала рассмотрим такую задачу. Предположим, есть следующий кусочек данных.{ products: [ { name: "All about JavaScript", type: "book", price: 22, discount: 20 } ] }
Постановка задачи
Найти финальную цену первого товара с учетом скидки. Если по какой-либо причине будут переданы неправильные данные, вывести строку "No data".Шаги алгоритма
- Найти первый продукт со скидкой,
- Применить скидку,
- Продолжать проверку данных на валидность. Если данные не валидны - вернуть "No data".
Традиционное решение
const isProductWithDiscount = product => { return !isNaN(product.discount) } const findFirstDiscounted = products => { products.find(isProductWithDiscount) } const calcPriceAfterDiscount = product => { return product.price - product.discount } const findFinalPrice = (data, fallbackValue) => { if(!data || !data.products) return fallbackValue const discountedProduct = findFirstDiscounted(data.products) if(!discountedProduct) return fallbackValue return calcPriceAfterDiscount(discountedProduct) } findFinalPrice(data, "No data")
Комментарии к традиционному решению
Достоинства:- Атомарные логические единицы (
isProductWithDiscount
,findFirstDiscounted
иcalcPriceAfterDiscount
), - Логику защищена от невалидных данных.
- Cлишком много защитных проверок. (Защитное программирование (Defensive programming) является обязательным в любом отказоустойчивом программном обеспечении. Однако, в нашем коде 50% тела функции
findFinalPrice
— проверка на валидность данных. Это слишком много). fallbackValue
почти везде.
Почему нас волнуют эти улучшения?
Потому что данный код заставляет слишком в него вникать. Это негативно влияет на DX (Developer Experience) - уровень удовлетворенности разработчика от работы с кодом. Проанализируем код, чтобы прийти к лучшему решению. Части, которые мы стремимся улучшить, формируют паттерны (защита (defence) и откат (fallback)). Хорошо то, что эти части на самом деле цельные и атомарные. Мы должны иметь возможность абстрагировать этот паттерн в оболочку, которая могла бы обрабатывать эти крайние случаи вместо нас. Обертка позаботится о крайних случаях, а нам останется позаботиться только о бизнес-логике.Функтор Maybe
Как мы обсуждали ранее, нам нужна только обертка, которая абстрагируется от обработки данных. Итак, роль функтора Maybe
состоит в том, чтобы обернуть наши данные (потенциально невалидные данные) и обработать для нас крайние случаи.
Имплементация функтора Maybe
function Maybe(value) { const isNothing = () => { return value === null || value === undefined } const map = (fn) => { return isNothing() ? Maybe() : Maybe(fn(value)) } const getValueOrFallback = { return (fallbackValue) => isNothing() ? fallbackValue : value; } return { map, getValueOrFallback, }; }
Пояснения к имплементации
isNothing
проверяет валидно ли обернутое в функторMaybe
значениеmap
- интерфейс для преобразования обернутого значения, с помощью которого мы применяем функции с бизнес логикой к обернутому значению.map
возвращает новое значение в другом экземпляреMaybe
. Таким образом, мы можем сделать цепочку вызововmap
-.map().map().map...
.getValueOrFallback
возвращает обернутое значение или запасное значениеfallbackValue
.
Как использовать функтор Maybe
?
С валидными данными:
С невалидными данными:Maybe('Hello') .map(x => x.substring(1)) .getValueOrFallback('fallback') // 'ello'
Maybe(null) .map(x => x.substring(1)) // функция не будет запущена .getValueOrFallback('fallback') // 'fallback'
Maybe
обработал крайние случаи вместо нас и не запустил функцию с невалидными данными. Нам нужно лишь позаботиться о бизнес логике. Таким образом, мы внедрили улучшение, о котором говорили в традиционном решении. Внедрим это решение в задачу.
Решение задачи с функтором Maybe
const isProductWithDiscount = product => { return !isNaN(product.discount) } const findFirstDiscounted = products => { return products.find(isProductWithDiscount) } const calcPriceAfterDiscount = product => { return product.price - product.discount } Maybe(data) .map((x) => x.products) .map(findFirstDiscounted) .map(calcPriceAfterDiscount) .getValueOrFallback("No data")
Комментарии к решению с функтором Maybe
Мы смогли улучшить традиционное решение при помощи функтора Maybe
:
- мы не защищаем код сами, вместо нас это делает функтор
Maybe
, - мы указали
fallbackValue
только один раз.
Maybe
соответствует определению функтора? Функтор Maybe
это:
- обертка над любым значением из JavaScript типов,
- предоставляет интерфейс для преобразования - метод
map
, - подчиняется законам функтора.
Законы функторов
Закон идентичности (Identity law)
Если при выполнении операции преобразования, значения в функторе преобразовываются сами на себя, результатом будет немодифицированный функтор.const m1 = Maybe(value) const m2 = Maybe(value).map(v => v) // m1 и m2 эквивалентны
Закон композиции (Composition law)
Если две последовательные операции преобразования выполняются одна за другой с использованием двух функций, результат должен быть таким же, как и при одной операции отображения с одной функцией, что эквивалентно применению первой функции к результату второй.const m1 = Maybe(value).map(v => f(g(v))) const m2 = Maybe(value).map(v => g(v)).map(v => f(v)) // m1 и m2 эквивалентны
Зачем использовать функторы?
- Абстракция над применением функции,
- Усиление композиции функций,
- Уменьшение количества защитного кода (как в функторе
Maybe
), - Более чистая структура кода,
- Переменные более явно указывают на то, что мы ожидаем (что
Maybe
моделирует значение, которое может присутствовать, а может и не присутствовать).
Что означает Абстракция над применением функции?
То, что мы передаем функцию (т.е.x => x.products
) в интерфейс преобразования (т.е. map
) обертки (т.е. Maybe
), и она знает, как позаботиться о себе (посредством своей внутренней реализации).
Нас не интересуют детали реализации оболочки, которые она содержит (детали реализации скрыты), и тем не менее мы знаем, как использовать обертку (Array
или Promise
), используя их интерфейсы преобразования (map
).
И это на самом деле крайне важно в мире программирования. Это снижает планку того, как много мы, как программисты, должны понимать, чтобы иметь возможность что-то сделать. Функторы могут быть реализованы на любом языке, поддерживающем функции высшего порядка (а таких в наши дни большинство).
Почему функторы не используются повсеместно?
Просто потому, что мы к ним не привыкли. До.map
(и .then
) мы мутировали массивы или перебирали их значения вручную. Но как только мы обнаружили .map
, мы начали адаптировать его в качестве нового инструмента преобразования. Я надеюсь, что, поняв ценность функторов, мы начнем чаще внедрять их в наши ежедневные задачи как привычный инструмент.
Функтор Maybe
- лишь пример функтора. Существует множество функторов, которые выполняет различные задачи. В этой статье мы рассмотрели самый простой из них, чтобы понять саму идею функторов.