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

Простая шина событий на JavaScript

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

Шины событий — это чрезвычайно полезный инструмент для разделения компонентов приложений.

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

Что такое шина событий (event bus)?

Шина событий реализует шаблон издатель/подписчик. Его можно использовать для разъединения компонентов приложения, так что компонент может реагировать на события, инициируемые другим компонентом, без их прямых зависимостей друг от друга. Им нужно только знать шину событий. Каждый подписчик может подписаться на определенное событие. Подписчик будет уведомлен, когда событие, на которое он подписан, будет опубликовано в шине событий. Издатель может публиковать события в шине событий, когда что-то происходит.

Реализация шины событий

В этой реализации подписчик (subscriber) является простой функцией. Функция вызывается, когда интересующее событие публикуется в шине событий. Для сопоставления события с подписчиками можно использовать простой объект. Формат данных в этом объекте будет следующим: { eventType: { id: callback } }. Например:
{  
  event1: {
    1: func1,
    2: func2
  },  
  event2: {
    3: func3
  }  
}
События event1 и event2 могут быть любого типа. В большинстве случаев имеет смысл использовать простые строки. Подписчики func1, func2 и func3 являются простыми JavaScript функциями.
func1 и func2 являются подписчиками, подписанными на события типа event1. func3 является подписчиком, подписанным на события типа event2.
Идентификаторы 1, 2 и 3 будут использоваться позже для отмены подписки на подписчиков.

Как подписаться на событие?

Функция subscribe принимает в качестве аргументов интересующее событие и подписчика. Функция возвращает объект, предоставляющий функцию отмены подписки - unsubscribe. Функция unsubscribe может быть вызвана для удаления зарегистрированного подписчика. В этом примере используем генератор id для получения уникальных идентификаторов.
Идентификаторы используются для быстрой идентификации (за время O(1)) подписчика при вызове функции unsubscribe.
Вместо использования генератора id можно рассмотреть реализацию на основе ES6 символов.
const subscriptions = {}
const getNextUniqueId = getIdGenerator()
function subscribe(eventType, callback) {
  const id = getNextUniqueId()

  // создаем новый элемент для eventType
  if(!subscriptions[eventType]) {
    subscriptions[eventType] = {}
  }
 
  // регистрируем функцию обратного вызова
  subscriptions[eventType][id] = callback
  return {
    unsubscribe: () => {
      delete subscriptions[eventType][id]
      if(Object.keys(subscriptions[eventType]).length === 0) {
        delete subscriptions[eventType]
      }
    }
  }
}

Как опубликовать событие?

Функция публикации принимает в качестве аргументов событие и аргументы для подписчиков. Если подписчиков на eventType нет, то просто завершаем функцию. В противном случае перебираются идентификаторы подписчиков, зарегистрированных для eventType, и вызывается каждая функция с предоставленными аргументами.
function publish(eventType, arg) {
  if(!subscriptions[eventType]) {
    return
  }
  Object.keys(subscriptions[eventType])
    .forEach(id => subscriptions[eventType][id](arg))
}

Пример использования

В качестве простого примера предположим, что мы хотим печатать что-то в консоли каждый раз, когда появляется событие типа print. Мы можем подписаться на событие следующим образом:
const subscription = EventBus.subscribe(
  "print",
  message => console.log(`printing: ${message}`)
)
Затем мы можем выпустить событие, подобное этому:
EventBus.publish("print", "some text")
Если в какой-то момент мы захотим прекратить прослушивание события с типом print, мы можем отписаться следующим образом:
subscription.unsubscribe()

Полная реализация

Вот полная реализация этой шины событий. Он может быть легко преобразован в простую функцию Javascript.
// eventBus.js

const subscriptions = {}
const getNextUniqueId = getIdGenerator()

function subscribe(eventType, callback) {
  const id = getNextUniqueId()

  if(!subscriptions[eventType]) {
    subscriptions[eventType] = {}
  }

  subscriptions[eventType][id] = callback

  return { 
    unsubscribe: () => {
      delete subscriptions[eventType][id]
      if(Object.keys(subscriptions[eventType]).length === 0) {
        delete subscriptions[eventType]
      }
    }
  }
}

function publish(eventType, arg) {
  if(!subscriptions[eventType]) {
    return
  }
  
  Object.keys(subscriptions[eventType]).forEach(key => subscriptions[eventType][key](arg))
}

function getIdGenerator() {
  let lastId = 0
  
  return function getNextUniqueId() {
    lastId += 1
    return lastId
  }
}

module.exports = { publish, subscribe }