Как использовать 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 можно использовать для улучшения функциональности кода и предоставления элегантных решений сложных проблем.

Итоги

JavaScript Proxy предлагает разработчикам мощный инструмент для перехвата и настройки операций с объектами в JavaScript. Используя концепцию ловушек, прокси обеспечивают детальный контроль над фундаментальными операциями, позволяя гибко и динамично изменять поведение. В этой статье мы рассмотрели основы использования прокси JavaScript, поняли, как определять ловушки. Мы также выяснили, как прокси можно использовать для проверки, безопасности, а также изучили их ограничения производительности. Вооружившись этими знаниями, теперь вы можете уверенно использовать JavaScript Proxy для создания надежных и безопасных приложений, используя его потенциал для настройки и контроля.

Что такое функтор? Функциональное программирование

2 года назад·5 мин. на чтение

В этой статье на простых и доступных примерах рассмотрим одну из концепций функционального программирования - Функтор.

Это серия статей о функциональном программировании:
  1. Парадигмы программирования
  2. Композиция
  3. Функторы (рассматривается в этой статье)
  4. Каррирование
  5. Чистые функции
  6. Функции первого класса

Что такое функтор?

Функтор (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".

Шаги алгоритма

  1. Найти первый продукт со скидкой,
  2. Применить скидку,
  3. Продолжать проверку данных на валидность. Если данные не валидны - вернуть "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 это:
  1. обертка над любым значением из JavaScript типов,
  2. предоставляет интерфейс для преобразования - метод map,
  3. подчиняется законам функтора.

Законы функторов

Закон идентичности (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 - лишь пример функтора. Существует множество функторов, которые выполняет различные задачи. В этой статье мы рассмотрели самый простой из них, чтобы понять саму идею функторов.

Итоги

Функтор как паттерн проектирования - это простой, но очень мощный паттерн. Мы используем его ежедневно в различных типах данных, не догадываясь об этом. Было бы здорово, если мы сможем распознавать и ценить функторы немного больше и выделять им больше места в кодовой базе, потому что они делают код чище и дают нам больше возможностей.