Question:
What are proxies useful for, and how often are they used in real-world development?
It is also not clear about reflections, it turns out it's almost the same. It is unclear why both exist at the same time.
Answer:
Proxy objects are "traps" with which we can intercept the "events" we need from objects, classes, functions, etc.
If you are completely unfamiliar with them, then I would say that this is very similar to the eventListners from the Browser API , since using the Proxy we can bind and, if necessary, intercept the event we need from those entities that are listed above.
What a trap like this looks like:
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
Now we get the values of the object, but we do it through the proxy, so we have the opportunity to do virtually anything with value before giving it:
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
That is, now we sort of set the eventListener to the get () "event", after it triggered, we intercepted a request for a specific key and changed its value to the format we need.
Now we can safely add new keys to our object:
addressProxy.street = "Arbat"
console.log(addressProxy)
// { country: "Russia", city: "Moscow", street: "Arbat" }
but it's easy to disable this by binding to the set () "event":
const addressProxy = new Proxy(address, {
set: (target, prop, value) => {
return false
}
})
addressProxy.street = "Arbat"
console.log(city in addressProxy)
// { country: "Russia", city: "Moscow" }
We can also hide certain fields:
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"
}
})
if now we "ask" whether there is such a field, we get:
console.log("country" in addressProxy)
// true
console.log("city" in addressProxy)
// true
console.log("metadata" in addressProxy)
// false
Functions can also be proxied, so we, for example, can track the moment when it will be called:
const print = text => console.log(text)
const printProxy = new Proxy(print, {
apply: (target, thisArg, argArray) => {
return target.apply(thisArg, argArray)
}
})
printProxy("это тест, мы успешно перехватили вызов функции")
// это тест, мы успешно перехватили вызов функции
which will allow us to easily implement the following logic:
// давайте отфильтруем плохие слова
// запретив нашей функции их вывод
// и оставим только приятный слуху язык
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("ругательство")
// ***
I think that now the basic idea of a proxy has become clearer. Separately, I want to note that for each of the entities there are handlers specific to them, for example, for functions it is apply () , and for classes (and everything that starts with the new operator ) construct () .
A complete list of handlers can be found here .
How often are they used in real-world development?
In large modern projects, proxying is used quite often, one of the common areas of application is various kinds of "optimization".
Suppose we have an array of users and we need to find the one we need by its 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: "Антон" }
In principle, no problem if there are only three users, but what should we do if there are 100,000 of them?
(a permanent bulkhead of this size would be very costly)
const users = [
{ id: 1, name: "Иван" },
{ id: 2, name: "Мария" },
{ id: 3, name: "Антон" }
// ...
// и еще более чем
// 100.000 записей
]
We can hardcode the Array class, and add a handler construct () to it , which will allow us to "bind" to the time of initialization of each new instanc'a.
Inside it, we will iterate over our array and assign each record an index equal to the user ID:
const IndexedArray = new Proxy(Array, {
construct: () => {
const index = {}
users.forEach(item => index[item.id] = item)
return index
}
})
const indexedUsers = new IndexedArray(users)
The iteration will be performed only once , at the time of creating a new instanc'a IndexedArray:
console.log(indexedUsers)
// {
// "1": { id: 1, name: "Иван" },
// "2": { id: 2, name: "Мария" },
// "3": { id: 3, name: "Антон" }
// ... остальные 100.000
// }
After that we will be able to get the user we need as simply as possible:
console.log(indexedUsers[3])
// { id: 3, name: "Антон" }
I intentionally simplified the above example so that it would be easy to understand the main idea, however, in order to preserve the full functionality, including adding / removing / changing fields, etc., it will need to be improved.
Proxy and Reflect'y are standard embedded objects but if the first is designed to "intercepting" and "overwriting" proxied fundamental operations of the object, the second provides methods for working with the "intercepted" operations.
All Reflect methods and properties are static, and the object itself is non-functional, which means that it is non-constructable and we cannot use it together with the new operator or call it as a function.
The names of the functions of the Reflect object have names identical to the names of handlers in Proxy, and some of them repeat the functionality of the methods of the Object class, albeit with some differences .
Why is this needed?
This is standardization.
For example, the apply () method is present for all constructors (many implementations), and with its placement in Reflect (one implementation).
Default settings ESLint already signaled that instead:
testFunction.apply(thisArg, argsArr)
worth using:
Reflect.apply(testFunction, thisArg, argsArr)
Also, for example, instead of writing:
const chinaAddress = new Address(argsArr)
// sidenote
// такой подход приведет к созданию
// и использованию итератора
can be written like this:
const chinaAddress = Reflect.construct(Address, argsArr)
// sidenote
// такой подход не потребует задействования итератора
// поскольку construct() использует
// length и прямой доступ
// что в целом положительно повлияет на оптимизацию
that is, choosing Reflect instead of standard methods, we are just riding the same wavelength in the direction of where modern JavaScipt is heading.