Зачем Proxy и Reflect в JavaScript, в чем разница и когда и что применять?

Для чего полезны Proxy, и насколько часто они применяются в реальной разработке?

Так же непонятно про рефлекты, получается это почти тоже самое. Непонятно зачем существует одновременно и то и то.


Ответы (3 шт):

Автор решения: Arturas Lapinskas

Это эксперементальные методы, возможно уберут потом. В реальных проектах ни разу не встречал. Вообще прокси - это один из классических паттернов программирования, но в js как-то привыкли обходится без него.

https://learn.javascript.ru/proxy

→ Ссылка
Автор решения: Grundy

Proxy - это конструктор, позволяющий сделать обертку над объектом, в которой можно переопределить стандартное поведение, например, обращение к свойству объекта

var target = {
  a: 1
};

var handler = {
  get(target, propertyName, proxy) {
    return target.hasOwnProperty(propertyName) ? target[propertyName] : 42;
  }
}


var proxy = new Proxy(target, handler);

console.log(proxy.a);
console.log(proxy.b);
console.log(target.b);

Как видно в примере, при обращению к свойству, которое отсутствует у целевого объекта, будет возвращено заранее указанное значение, а не undefined.


Reflect - это объект, который предоставляет методы, для работы с объектом, в нем дублируются некоторые методы из Object, например

Object.defineProperty - Reflect.defineProperty

А так же методы, дублирующие функциональность некоторых операторов,

Таким образом в этом объекте решили собрать методы для работы с внутренностями объекта.


Proxy активно использует Vuejs

→ Ссылка
Автор решения: Vasily

Объекты Proxy это "ловушки", с помощью которых мы можем перехватывать нужные нам "события" у объектов, классов, функций, и пр.

Если Вы совсем с ними не знакомы, то я бы сказал что это очень похоже на eventListner'ы из Browser'ного API, поскольку с помощью Proxy мы можем привязаться и, при необходимости перехватить нужное нам событие у тех entity, которые перечислены выше.

Как выглядит такая ловушка:

const address = {
  country: "Russia",
  city: "Moscow"
}

const addressProxy = new Proxy(address, {
  // здесь мы определяем какое именно действие
  // по объекту address мы хотим перехватить

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

  // для этого мы поставим ловушку/handler
  get: (target, prop) => {
    return target[prop]
  }
})

console.log(addressProxy.country)
// Russia

console.log(addressProxy.city)
// Moscow

console.log(addressProxy.street)
// undefined

Сейчас мы получаем значения объекта, но делаем это уже через прокси, соотвественно у нас есть возможность сделать с value фактически что угодно перед тем как его отдать:

const addressProxy = new Proxy(address, {
  get: (target, prop) => {
      if (prop === "country") {
          return target[prop].slice(0, 2).toUpperCase()
      }
      return target[prop]
  }
})

console.log(addressProxy.country)
// RU

console.log(addressProxy.city)
// Moscow

console.log(addressProxy.street)
// undefined

Т.е сейчас мы как бы поставили eventListener на "событие" get(), после того как он сработал перехватили запрос к конкретному ключу и изменили его значение на нужный нам формат.

Сейчас мы можем спокойно добавлять новые ключи в наш объект:

addressProxy.street = "Arbat"

console.log(addressProxy)
// { country: "Russia", city: "Moscow", street: "Arbat" }

но это легко это запретить, привязавшись к "событию" set():

const addressProxy = new Proxy(address, {
  set: (target, prop, value) => {
      return false
  }
})

addressProxy.street = "Arbat"

console.log(city in addressProxy)
// { country: "Russia", city: "Moscow" }

Так же мы можем скрывать определенные поля:

addressProxy.metadata = 12345

console.log(addressProxy)
// { country: "Russia", city: "Moscow", metadata: 12345 }

const addressProxy = new Proxy(address, {
  has: (target, prop) => {
      return prop in target && prop !== "metadata"
  }
})

если теперь мы "спросим" есть ли такое поле, то получим:

console.log("country" in addressProxy)
// true

console.log("city" in addressProxy)
// true

console.log("metadata" in addressProxy)
// false

Функции тоже можно проксировать, так мы, например, можем отследить момент когда она будет вызвана:

const print = text => console.log(text)

const printProxy = new Proxy(print, {
  apply: (target, thisArg, argArray) => {
      return target.apply(thisArg, argArray)
  }
})

printProxy("это тест, мы успешно перехватили вызов функции")
// это тест, мы успешно перехватили вызов функции

что позволит нам без лишних усилий реализовать следующую логику:

// давайте отфильтруем плохие слова
// запретив нашей функции их вывод
// и оставим только приятный слуху язык

const print = text => console.log(text)

const printProxy = new Proxy(print, {
  apply: (target, thisArg, argArray) => {
      // для простоты примера представим что 
      // в массиве запрещенных слов сейчас 
      // только одно слово
      const badWords = ["ругательство"]
      if (badWords.includes(argArray[0])) {
        return target("***")
      }
      return target(argArray[0])
  }
})

printProxy("спасибо")
// спасибо

printProxy("ругательство")
// ***

Думаю что теперь основная идея прокси стала более понятна. Отдельно хочу отметить что для каждого из entity присутствуют специфичные им handler'ы, например для функций это apply(), а для классов (и всего того что начинается с оператора new) construct().

Полный список handler'ов можно посмотреть здесь.

Насколько часто они применяться в реальной разработке?

В крупных современных проектах проксирование применяется достаточно часто, одна из распространенных областей применения это разного рода "оптимизации".

Предположим у нас есть массив пользователей и нам нужно найти нужного по его ID:

const users = [
  { id: 1, name: "Иван" },
  { id: 2, name: "Мария" },
  { id: 3, name: "Антон" }
]

// что бы найти пользователя с ID равным 3
// нам нужно пройтись по всему массиву
// и сверить ID у каждого пользователя
// пока мы не найдем нужный

const targetUser = users.find(user => user.id === 3)

console.log(targetUser)
// { id: 3, name: "Антон" }

В принципе никаких проблем если пользователей всего три, но что нам делать если их 100.000?

(постоянная переборка такого объема будет очень затратной)

const users = [
  { id: 1, name: "Иван" },
  { id: 2, name: "Мария" },
  { id: 3, name: "Антон" }
  // ...
  // и еще более чем
  // 100.000 записей
]

Мы можем запроксировать класс Array, и добавить ему handler construct(), который позволит нам "привязаться" к моменту инициализации каждого нового instanc'a.

Внутри него мы проитерируем наш массив и присвоим каждой записи индекс равный ID пользователя:

const IndexedArray = new Proxy(Array, {
  construct: () => {
    const index = {}
      users.forEach(item => index[item.id] = item)
      return index
  }
})

const indexedUsers = new IndexedArray(users)

Итерация будет произведена только один раз, в момент создания нового instanc'a IndexedArray:

console.log(indexedUsers)
// {
//   "1": { id: 1, name: "Иван" },
//   "2": { id: 2, name: "Мария" },
//   "3": { id: 3, name: "Антон" }
//   ... остальные 100.000
// }

После чего мы сможем получать нужного нам пользователя максимально просто:

console.log(indexedUsers[3])
// { id: 3, name: "Антон" }

Я намеренно упростил приведенный выше пример для того что бы было легко понять основную идею, однако что бы сохранить полный функционал, включающий добавление/удаление/изменение полей и пр. его потребуется доработать.


Proxy и Reflect'ы являются стандартными встроенными объектами, но если первый предназначен для "перехватывания" и "переписывания" фундаментальных операций проксируемого объекта, то второй предоставляет методы для работы с "перехватываемыми" операциями.

Все методы и свойства Reflect'ов статичны, а сам объект нефункциональный, из чего следует что он неконструируем и мы не сможем использовать его вместе с оператором new или вызвать как функцию.

Имена функций объекта Reflect имеют названия идентичные названиям handler'ов в Proxy, а некоторые из них повторяют фунционал методов класса Object, правда, с некоторыми отличиями.

Зачем это нужно?

Это стандартизация.

Например метод apply() присутствует у всех конструкторов (много реализаций), а с вынесением его в Reflect (одна реализация).

Дефолтные настройки ESLint уже сигнализируют о том что вместо:

testFunction.apply(thisArg, argsArr)

стоит использовать:

Reflect.apply(testFunction, thisArg, argsArr)

Так же, например, вместо того что бы писать:

const chinaAddress = new Address(argsArr)
// sidenote
// такой подход приведет к созданию 
// и использованию итератора

можно написать так:

const chinaAddress = Reflect.construct(Address, argsArr)
// sidenote
// такой подход не потребует задействования итератора
// поскольку construct() использует
// length и прямой доступ
// что в целом положительно повлияет на оптимизацию

т.е выбирая Reflect вместо стандартных методов мы просто едем на одной волне в сторону того куда движется современный JavaScipt.

→ Ссылка