Руководство для начинающих по пониманию и использованию возможностей прокси ES6.

В ES6 (ECMAScript 2015) появилась мощная функция под названием Proxies to JavaScript. Прокси позволяют нам определять собственное поведение для фундаментальных операций с объектами, таких как поиск свойств, назначение и вызов функций. Хотя прокси-серверы не могут широко использоваться в повседневном программировании, они могут быть невероятно полезны в определенных сценариях, где требуется детальный контроль над поведением объекта.

Понимание прокси

Прокси в JavaScript действует как посредник между кодом и объектом, с которым он взаимодействует. Когда мы создаем прокси, мы определяем набор обработчиков, которые перехватывают и переопределяют поведение операций объекта по умолчанию. Эти обработчики позволяют нам настраивать доступ к свойствам, их изменение и вызов в целевом объекте. Как только прокси создан, он становится основным средством взаимодействия с объектом, и все операции над объектом проходят через прокси.

Давайте начнем с простого примера, чтобы проиллюстрировать концепцию прокси:

const target = {
  value: 42
};

const handler = {
  get: function(target, prop) {
    console.log(`Getting property '${prop}'`);
    return target[prop];
  },
  set: function(target, prop, value) {
    console.log(`Setting property '${prop}' to '${value}'`);
    target[prop] = value;
  }
};
const proxy = new Proxy(target, handler);
console.log(proxy.value); // Output: Getting property 'value', 42
proxy.value = 100; // Output: Setting property 'value' to '100'
console.log(proxy.value); // Output: Getting property 'value', 100

В этом примере мы создаем прокси proxy, который обертывает объект target. Мы определяем объект handler двумя методами: get и set. Метод get перехватывает доступ к свойству и регистрирует сообщение, прежде чем вернуть фактическое значение свойства. Метод set перехватывает назначение свойства, регистрирует сообщение и обновляет значение свойства целевого объекта.

Когда мы обращаемся к proxy.value, запускается обработчик get, регистрирующий сообщение и возвращающий значение 42 из объекта target. Точно так же, когда мы назначаем proxy.value = 100, запускается обработчик set, регистрирующий сообщение и обновляющий значение target.value до 100. Наконец, повторный доступ к proxy.value запускает обработчик get, который регистрирует сообщение и возвращает обновленное значение 100.

Наблюдение за состоянием объекта

Одной из мощных возможностей прокси является возможность наблюдать и отслеживать изменения состояния объекта. Перехватывая операцию set, мы можем получать уведомления всякий раз, когда свойство изменяется, и предпринимать соответствующие действия. Рассмотрим пример наблюдения за состоянием объекта с помощью прокси:

function observe(obj, callback) {
  return new Proxy(obj, {
    set: function(target, prop, value) {
      const oldValue = target[prop];
      target[prop] = value;
      callback({ name: prop, oldValue, newValue: value });
      return true;
    }
  });
}

const person = { name: 'Alice', age: 25 };
const observedPerson = observe(person, ({ name, oldValue, newValue }) => {
  console.log(`Property '${name}' changed from '${oldValue}' to '${newValue}'`);
});
observedPerson.age = 30; // Output: Property 'age' changed from '25' to '30'

В этом примере мы определяем функцию observe, которая принимает обработчик объекта objand a callbackfunction as parameters. Theobservefunction creates a proxy around theobjusing theProxyconstructor. We specify theset`, который перехватывает присвоение свойств.

Внутри обработчика set мы сохраняем старое значение свойства, обновляем значение свойства целевого объекта, а затем вызываем функцию callback с объектом, содержащим имя измененного свойства, старое значение и новое значение. Наконец, мы возвращаем true, чтобы указать, что операция прошла успешно.

В примере мы создаем объект person со свойствами name и age. Мы создаем наблюдаемую версию объекта person, вызывая функцию observe и передавая person и функцию обратного вызова. Функция обратного вызова регистрирует сведения об изменении свойства.

Когда мы присваиваем новое значение observedPerson.age, срабатывает обработчик set. Он фиксирует старое значение (25), обновляет свойство age до 30 объекта person и вызывает функцию обратного вызова с именем свойства, старым значением и новым значением. Обратный вызов регистрирует изменение свойства, отображая имя свойства, старое значение и новое значение.

Используя прокси, мы можем легко наблюдать за изменениями состояния объекта и выполнять пользовательские действия всякий раз, когда изменяется свойство.

Проверка свойств набора

Другое полезное применение прокси — проверка свойств. Прокси-серверы можно использовать для обеспечения соблюдения определенных правил и ограничений при назначении свойств, гарантируя, что присвоенные значения соответствуют определенным критериям. Давайте рассмотрим пример проверки свойств с использованием прокси:

function withValidation(obj, schema) {
  return new Proxy(obj, {
    set: function(target, prop, value) {
      if (prop in schema && Array.isArray(schema[prop])) {
        const validators = schema[prop];
        const isValid = validators.every(validator => validator(value));
        if (isValid) {
          target[prop] = value;
        } else {
          throw new Error(`Invalid value for property '${prop}'`);
        }
      } else {
        target[prop] = value;
      }
      return true;
    }
  });
}

const data = {
  name: 'John Doe',
  age: 30
};
const schema = {
  name: [value => typeof value === 'string'],
  age: [value => typeof value === 'number', value => value >= 18]
};
const validatedData = withValidation(data, schema);
validatedData.name = 'Jane Smith';
validatedData.age = 15; // Throws an error: Invalid value for property 'age'

В этом примере мы определяем функцию withValidation, которая принимает объект obj и объект schema в качестве параметров. Объект schema содержит правила проверки для каждого свойства файла obj. Ключи объекта schema соответствуют именам свойств, а значения представляют собой массивы функций проверки.

Внутри обработчика set прокси мы проверяем, существует ли присваиваемое свойство (prop) в объекте schema и есть ли у него связанные валидаторы. Если валидаторы существуют, мы перебираем их, используя метод every, чтобы убедиться, что все валидаторы возвращают true для присвоенного значения. Если все валидаторы проходят успешно, мы обновляем значение свойства целевого объекта (target[prop] = value). В противном случае мы выдаем ошибку, указывающую, что значение недействительно.

В примере у нас есть объект data со свойствами name и age. Мы определяем объект schema, который определяет правила проверки для каждого свойства. Свойство name должно быть строкой, а свойство age должно быть числом, большим или равным 18.

Мы создаем объект validatedData, вызывая функцию withValidation и передавая объект data и файл schema. Когда мы присваиваем новое значение validatedData.name, срабатывает обработчик set. Он проверяет schema на наличие свойства name и обнаруживает, что у него есть единственный валидатор, который проверяет, является ли значение строкой. Поскольку присвоенное значение ('Jane Smith') является строкой, присвоение выполнено успешно, и значение свойства обновлено.

Однако, когда мы присваиваем значение validatedData.age, обработчик set запускается снова. На этот раз schema для свойства age содержит два валидатора: один проверяет, является ли значение числом, а другой проверяет, больше или равно ли значение 18. Поскольку присвоенное значение (15) не проходит второй валидатор, который проверяет требования к минимальному возрасту, выдается ошибка с сообщением «Неверное значение свойства «возраст».

Используя прокси для проверки свойств, мы можем применять ограничения и правила для назначений объектов, гарантируя, что назначенные значения соответствуют указанным критериям проверки.

Делаем код ленивым

Прокси-серверы ES6 также можно использовать для реализации ленивой оценки кода. Ленивая оценка откладывает выполнение кода до тех пор, пока он действительно не понадобится, что потенциально экономит вычислительные ресурсы. Прокси позволяют нам перехватывать вызовы методов и откладывать их выполнение до тех пор, пока это не потребуется. Давайте рассмотрим пример создания ленивого кода с помощью прокси:

class Calculator {
  add(a, b) {
    return a + b;
  }

  multiply(a, b) {
    return a * b;
  }
}

function lazify(obj) {
  const operations = [];
  const handler = {
    get(target, prop) {
      const method = target[prop];
      if (typeof method === 'function') {
        const wrapper = (...args) => {
          operations.push({ method, args });
          return proxy;
        };
        return wrapper;
      }
      return method;
    }
  };

  const proxy = new Proxy(obj, handler);
  proxy.run = () => {
    let result = null;
    for (const operation of operations) {
      const { method, args } = operation;
      result = method.apply(obj, args);
      if (result === '$') {
        result = obj;
      }
    }
    return result;
  };
  return proxy;
}

const calc = new Calculator();
const lazyCalc = lazify(calc);
lazyCalc.add(2, 3).multiply(4).add(5, 1).run(); // Output: 26

В этом примере у нас есть простой класс Calculator с двумя методами: add и multiply. Функция lazify берет объект obj и создает вокруг него прокси. Мы определяем объект handler с методом get, который перехватывает доступ к свойствам.

Внутри метода get мы проверяем, является ли доступное свойство функцией. Если это так, мы создаем функцию-оболочку, которая захватывает метод и его аргументы. Функция-оболочка добавляет операцию в массив operations и возвращает сам прокси (proxy), чтобы включить цепочку методов.

Метод proxy.run отвечает за выполнение операций. Он перебирает массив operations, применяет каждый метод с его аргументами к исходному объекту (obj) и сохраняет результат. Если результатом является постоянный символ $, мы заменяем его исходным объектом (obj). Наконец, метод run возвращает окончательный результат.

В этом примере мы создаем экземпляр класса Calculator и оборачиваем его функцией lazify, чтобы создать ленивую версию (lazyCalc). Затем мы связываем несколько вызовов методов (add, multiply, add) для lazyCalc и вызываем метод run. Метод run выполняет операции в указанном порядке, выполняя вычисления и возвращая окончательный результат.

В этом примере связанные вызовы методов add(2, 3).multiply(4).add(5, 1) представляют серию операций: сложение 2 и 3, умножение результата на 4 и добавление 5 и 1 к предыдущему результату. Метод run выполняет эти операции и возвращает окончательный результат, который равен 26.

Используя прокси, чтобы сделать код ленивым, мы можем отложить выполнение операций до тех пор, пока они не потребуются, что позволит более эффективно использовать ресурсы и избежать ненужных вычислений.

Заключение

Прокси-серверы ES6 — это мощная функция, которая позволяет нам определять собственное поведение для операций с объектами в JavaScript. Они обеспечивают детальный контроль над поиском свойств, назначением и вызовом функций, позволяя нам создавать динамичный и гибкий код.

Хотя прокси-серверы обычно не используются в повседневном программировании, они могут быть невероятно полезны в определенных сценариях, где нам нужно настроить поведение объекта или реализовать расширенные функции. Однако важно учитывать последствия использования прокси-серверов для производительности, поскольку они могут привести к накладным расходам по сравнению с обычными операциями с объектами.

Спасибо за прочтение. Если вам понравилась эта статья, поддержите меня, став участником Medium.

Повышение уровня кодирования

Спасибо, что являетесь частью нашего сообщества! Перед тем, как ты уйдешь:

  • 👏 Хлопайте за историю и подписывайтесь на автора 👉
  • 📰 Смотрите больше контента в публикации Level Up Coding
  • 💰 Бесплатный курс собеседования по программированию ⇒ Просмотреть курс
  • 🔔 Подписывайтесь на нас: Twitter | ЛинкедИн | "Новостная рассылка"

🚀👉 Присоединяйтесь к коллективу талантов Level Up и найдите прекрасную работу