Це розповідь про обʼєкт Proxy
, який зʼявився в ES6.
А розповідь почнемо не з теорії, а прикладу
Діставання елементів масиву завдяки відʼємним індексам
В python
є можливість діставати елементи масиву, використовуючи відʼємні індекси:
letters = ["a", "b", "c"]
print(letters[-1]) # => 'c'
print(letters[-2]) # => 'b'
Було б добре, такий спосіб діставання елементів масиву додати до JS 🤔 (вважаймо цей спосіб альтернативою до наявного методу at масивів в JS)
Оскільки це розповідь про Proxy
, то зрозуміло, що цю задачу можна розвʼязати завдяки Proxy
:
function createArray(...items) {
const handler = {
get(target, property, receiver) {
let index = parseInt(property, 10);
if (index < 0) {
index = target.length + index;
}
return target[index];
},
};
return new Proxy(items, handler);
}
const numbers = createArray(1, 2, 3, 4, 5, 6);
console.log(numbers[-1]); // => 6
Щоб зрозуміти, як працює приклад вище, потрібно трохи заглибитись в теорію
Теорія
Що потрібно знати про Proxy?
Додаючи Proxy до обʼєкту, ми створюємо додаткову обгортку. Маючи таку обгортку, ми можемо перехоплювати та перевизначати базові методи для роботи з обʼєктами
На діаграмі нижче проілюстрований принцип того, як працює проксі для обʼєкту Person
. В цьому випадку в PROXY
було перевизначено метод get
. Тому звертаючись до поля Peson.name
звернення спочатку йде до PROXY
, а звідти, вже до обʼєкту Person
PROXY Person
+---------------+ +---------------+
| | | |
| | | name |
Person.name | +-----+ | | +-----+ |
----------------|--->| |----|---------|--->| | |
| | get | | | | Lil | |
-------<--------|----| |<---|---------|----| | |
Lil | +-----+ | | +-----+ |
| | | |
| | | |
+---------------+ +---------------+
Розглянемо на реальному прикладі, як можна перехопити та перевизначити внутрішній метод [[Get]]
обʼєкту
Логування звернень до полів обʼєкту
В коді нижче оголошується функція traceProperty
, яка першим аргументом приймає обʼєкт, а інші аргументи являють собою назви полів, звернення до яких ми хочемо логувати. Найважливіша частина, це метод handler.get
в середині функції traceProperty
. Саме метод handler.get
перехоплює і перевизначає, як відбувається звернення до полів обʼєкту traceMagic
const magic = {
number: 42,
isMagic: false,
toString() {
console.log(
`Your number is ${this.number}. Am I right? Answer: ${this.isMagic}`
);
},
};
const traceMagic = traceProperty(magic, 'isMagic', 'number');
const guestNumber = traceMagic.number; // Get property magicNumber
traceMagic.toString(); // Get property magicNumber, Get property isMagic
function traceProperty(obj, ...keys) {
const handler = {
get(target, propKey) {
if (keys.includes(propKey)) {
console.log(`Get property ${propKey}`);
}
return target[propKey];
},
};
return new Proxy(obj, handler);
}
Як створювати Proxy?
Вище було розглянуто два приклади використання обʼєкту Proxy
. Тепер детальніше сфокусуємось, як саме створюється Proxy
та які параметри приймає цей обʼєкт.
Proxy створюється за наступним шаблоном:
const target = { msg: 'hello' };
const handler = {};
const proxy = new Proxy(target, handler);
+--------+ +---------+
| target | | handler |
+--------+ +---------+
| |
+----------------------+ +-----------------------+
| |
| |
+--------------------------------+ +--------------------------------+
| Оригінальний обʼєкт | | Обʼєкт, в якому вказуємо які |
| до якого хочемо додати Proxy | | внутрішні методи будемо |
+--------------------------------+ | перехоплювати та перевизначати |
+--------------------------------+
Що таке внутрішні методи обʼєкту?
В кожного обʼєкта є внутрішні методи, які визначають, як буде відбуватися взаємодія з обʼєктом.
Наприклад, коли дістаємося до значення поля obj.x
відбувається один з наступних сценаріїв:
- Шукаємо поле
x
в ланцюжку прототипів (prototype chain), допоки поле не буде знайдене - Якщо
x
додано черезObject.defineProperty()
, то повертаємо значення атрибутаvalue
- Поле
x
може бути геттером —>get x() {}
. В такому випадку виконуємо гетер і повертаємо значення
Тому синтаксис obj.x
виконує метод [[GET]]
, а обʼєкт використовує його власну внутрішню реалізацію цього методу, щоб визначити яке значення повернути. (ця частина тексту взята з mdn)
Які ще можуть бути внутрішні методи?
Це може бути сетер ☘️🐶 obj.y = 2
. Або ж has 🏎️ propKey in obj
Trap
Коли мова йде про Proxy
, то часто згадується термін trap
(пастка). Trap — це функція, яка визначає як буде поводити себе відповідний внутрішній метод обʼєкту, коли ми створюємо проксі
Ми їх вже зустрічали раніше:
const target = { msg: 'hello' };
const handler = {
get() {} // <---- це trap
has() {} // <---- і це trap
};
const proxy = new Proxy(target, handler);
На mdn є табличка, де зображені всі внутрішні методи обʼєкта та їх відповідні пастки
Reflect
Reflect
— це остання частина теорії, про яку варто згадати, перед тим, як рухатися до прикладів.
Згадаймо функцію, для логувати звернення до полів обʼєкту, про яку говорили раніше:
function traceProperty(obj, ...keys) {
const handler = {
get(target, propKey) {
if (keys.includes(propKey)) {
console.log(`Get property ${propKey}`);
}
return target[propKey];
},
};
return new Proxy(obj, handler);
}
В пастці get
ми спочатку логуємо назву поля, до якого відбувається звернення, а після цього власноруч виконуємо діставання елементу з обʼєкту target[propKey]
. До ES6
разом з Proxy
було додано обʼєкт Reflect
, який включає методи, що допомагають відтворити поведінку внутрішніх методів обʼєкту. Звучить складно, проте на практиці простіше. Наприклад, код вище з використанням Reflect
буде мати вигляд:
function traceProperty(obj, ...keys) {
const handler = {
get(target, propKey, receiver) {
if (keys.includes(propKey)) {
console.log(`Get property ${propKey}`);
}
return Reflect.get(target, propKey, receiver); // або Reflect.get(...arguments);
},
};
return new Proxy(obj, handler);
}
Інший приклад (джерело):
const handler = {
deleteProperty(target, propKey) {
return Reflect.deleteProperty(target, propKey); // щоб не писати delete target[propKey];
},
has(target, propKey) {
return Reflect.has(target, propKey); // щоб не писати propKey in target;
},
};
Для кожної пастки: handler.trap(target, arg_1, ···, arg_n)
Reflect
має відповідний метод: Reflect.trap(target, arg_1, ···, arg_n)
Підсумуємо теорію
-
Proxy
дозволяє перехоплювати та перевизначати поведінку внутрішніх методів обʼєкта, наприклад таких якget
абоset
. -
Приклад створення
Proxy
:
const target = { msg: 'hello' };
const handler = {
ownKeys() {} // <---- це trap
has() {} // <---- і це trap
};
const proxy = new Proxy(target, handler);
-
Використовуємо
Reflect.[trap]
для відтворення семантичного виклику (the reflective semantics for invoking) відповідного внутрішнього метода обʼєкта.Наприклад
'propName' in obj
—>Reflect.has(obj, 'propName')
абоobj.propName
—>Reflect.get(obj, 'propName')
Приклади використання
Ховання "приватних" полів
В JavaScript зʼявилась нова фіча — приватні поля. До появи цієї фічі, розробники могли домовлятися між собою, що умовно, в обʼєкті приватними полями будуть ті, які починаються з нижнього підкреслення, наприклад _name
.
const person = {
age: 42,
name: 'Philip',
_allergy: 'fish', // приватне поле за нашими домовленостями
};
З усім тим, це були "умовно" приватні поля, тому їх значення можна змінювати — peson._allergy = ''
.
Завдяки Proxy
, їх можна зробити по-справжньому приватними:
const data = {
age: 42,
name: 'Philip',
_allergy: 'fish',
};
const person = new Proxy(data, {
get(target, propertyKey, receiver) {
if (propertyKey.startsWith('_')) {
throw new SyntaxError('Could not get private member.');
}
return Reflect.get(target, propertyKey, receiver);
},
set(target, propertyKey, value, receiver) {
if (propertyKey.startsWith('_')) {
throw new SyntaxError('Could not set value for private member.');
}
return Reflect.set(target, propertyKey, value, receiver);
},
ownKeys(target) {
return Reflect.ownKeys(target).filter(function (name) {
return !name.startsWith('_');
});
},
});
console.log(Object.keys(person)); // ["age", "name"]
console.log(person._allergy); // "SyntaxError: Could not get private member.
person._allergy = ''; // "SyntaxError: Could not set value for private member.
Оскільки "умовно" приватні поля починаються з символа _
, то у кожній пастці ми перевіряємо, чи назва поля починається з цього символу. Якщо починається, то пастка кидає помилку
Нескінченний обʼєкт
Можливо ви опинялись в подібній ситуації. Спокійно працюєте з обʼєктом data
, а потім раптом хочете додати нове поле з певним значенням — data.person.name = 'Lil';
. Результат — отримуємо помилку Uncaught TypeError: Cannot read properties of undefined
Завдяки Proxy є можливість створити обʼєкт, якому така помилка не загрожує:
function infinityObject() {
return new Proxy(
{},
{
get(target, propertyKey, receiver) {
if (!Reflect.has(target, propertyKey)) {
target[propertyKey] = infinityObject();
}
return Reflect.get(target, propertyKey, receiver);
},
}
);
}
const dog = infinityObject();
dog.head.ear.size = 'small'; // присвоєно значення 'small'
console.log(dog.body.legs.top.right); // виведе в консоль `Proxy {}`
В даному прикладі створюється рекурсивне Proxy
. При зверненні до неіснуючого поля обʼєкту, створюється це поле зі значенням, яке повертає infinityObject()
, тобто запроксьований порожній обʼєкт.
Примітки
Чи є можливість заполіфілити Proxy
?
Частково. Наприклад, proxy-polyfill від команди Google Chrome
вміє полфілити такі пастки: get
, set
, apply
, consturctor
Чи можна відвʼязати Proxy
від обʼєкта?
Так, якщо проксі створене через Proxy.revocable()
:
const target = {};
const handler = {};
const { proxy, revoke } = Proxy.revocable(target, handler);
proxy.city = 'Kyiv'; // set 'city'
revoke();
proxy.prop; // it will throw an error
Які бібліотеки використовують Proxy
?
Додаткові матеріали
- Розділ книги по js від
2ality
присвячений Proxy Awesome list
про проксі