Proxy
Об'єкт Proxy
(заступник, прокладка) дає змогу створювати заступника для іншого об'єкта, котрий може перехоплювати й перевизначати базові операції для такого іншого об'єкта.
Опис
Об'єкт Proxy
дає змогу створити об'єкт, котрий буде вживатися на місці вихідного об'єкта, але може перевизначити базові операції Object
, як то отримання, присвоєння й означення властивостей. Об'єкти Proxy, загалом, використовують для журналювання звертань до властивостей, валідації, форматування чи оздоровлення введених даних, і так далі.
Для створення об'єкта Proxy
необхідні два параметри:
target
(ціль): вихідний об'єкт, котрий необхідно заступитиhandler
(обробник): об'єкт, котрий визначає те, які операції будуть перехоплені, і як перевизначити ці операції.
Наприклад, такий код створює заступника для об'єкта target
.
const target = {
message1: "привіт",
message2: "усім",
};
const handler1 = {};
const proxy1 = new Proxy(target, handler1);
Через те, що обробник – порожній, цей заступник поводиться так само, як сама ціль заступництва:
console.log(proxy1.message1); // привіт
console.log(proxy1.message2); // усім
Для налаштування заступника слід визначити на об'єкті-обробнику функції:
const target = {
message1: "привіт",
message2: "усім",
};
const handler2 = {
get(target, prop, receiver) {
return "світе";
},
};
const proxy2 = new Proxy(target, handler2);
Тут задана реалізація обробника get()
, котра перехоплює спроби звернутися до властивостей цілі.
Функції-обробники іноді називають пастками – здогадно, через те, що вони вловлюють виклики до цільового об'єкта. Вкрай проста пастка у handler2
вище перевизначає усі аксесори властивостей:
console.log(proxy2.message1); // світе
console.log(proxy2.message2); // світе
Заступники нерідко застосовуються вкупі з об'єктом Reflect
, котрий надає кілька методів з назвами, що збігаються з назвами пасток Proxy
. Методи Reflect
надають семантику рефлексії для закликання відповідних внутрішніх методів об'єктів. Наприклад, можна викликати Reflect.get
, коли немає потреби перевизначати поведінку об'єкта:
const target = {
message1: "привіт",
message2: "усім",
};
const handler3 = {
get(target, prop, receiver) {
if (prop === "message2") {
return "світе";
}
return Reflect.get(...arguments);
},
};
const proxy3 = new Proxy(target, handler3);
console.log(proxy3.message1); // привіт
console.log(proxy3.message2); // світе
Метод Reflect
так само взаємодіє з об'єктом через приховані методи об'єкта – він не "знезаступнює" заступника, бувши закликаним на заступнику. Коли методи Reflect
застосовуються в межах ловця заступника, і виклик методу об'єкта Reflect
знову перехоплюється пасткою, то може статися нескінченна рекурсія.
Термінологія
При мові про функціональність заступників вживаються наступні терміни.
- обробник
Об'єкт, переданий другим аргументом у конструктор
Proxy
. Містить пастки, котрі визначають поведінку заступника.- пастка
Функція, котра визначає поведінку відповідних внутрішніх методів об'єктів. (Це аналогічно концепції пасток в операційних системах.)
- ціль
Об'єкт, котрого віртуалізує заступник. Нерідко використовується заступником як бекенд. Інваріанти (семантика, що залишається без змін) щодо нерозширюваності об'єкта або неналаштовності властивостей перевіряються відносно цілі.
- інваріанти
Семантика, що залишається без змін при реалізації власних операцій. Якщо ваша реалізація пастки суперечить інваріантам обробника, то буде викинуто
TypeError
.
Внутрішні методи об'єктів
Об'єкти – це колекції властивостей. Проте мова JavaScript не надає жодних механізмів для безпосередньої маніпуляції даними, збереженими в об'єкті – натомість об'єкт визначає певні внутрішні методи, котрі задають те, як з ним можна взаємодіяти. Наприклад, при зчитуванні obj.x
можна очікувати на наступне:
- Властивість
x
шукається у ланцюжку прототипів, поки не знаходиться. - Якщо
x
є властивістю даних, то повертається атрибут дескриптора властивостіvalue
. - Якщо
x
є аксесорною властивістю, то закликається гетер, і його повернене значення – повертається.
В цьому процесі у мові програмування немає нічого особливого – лишень через те, що звичайні об'єкти усталено мають внутрішній метод [[Get]]
, в котрому задана така логіка. Синтаксис звертання до властивості obj.x
просто закликає на об'єкті метод [[Get]]
, і об'єкт використовує власну реалізацію внутрішнього методу для з'ясування того, що він поверне.
Інший приклад: масиви відрізняються від звичайних об'єктів, адже мають магічну властивість length
, котра, бувши зміненою, автоматично виділяє нові порожні комірки чи прибирає з масиву елементи. Подібно до цього, додавання до масиву елементів автоматично змінює властивість length
. Так відбувається через те, що масиви мають внутрішній метод [[DefineOwnProperty]]
, котрий знає, що треба оновити length
, коли виконується запис за цілочисловим індексом, або оновити вміст масиву, якщо вносяться зміни до властивості length
. Такі об'єкти, чия реалізація внутрішніх методів відрізняється від реалізації у звичайних об'єктах, звуться екзотичними об'єктами. Proxy
дає розробникам змогу використовувати повні можливості з визначення власних екзотичних об'єктів.
Усі об'єкти мають наступні внутрішні методи:
Внутрішній метод | Відповідна пастка |
---|---|
[[GetPrototypeOf]] |
getPrototypeOf() |
[[SetPrototypeOf]] |
setPrototypeOf() |
[[IsExtensible]] |
isExtensible() |
[[PreventExtensions]] |
preventExtensions() |
[[GetOwnProperty]] |
getOwnPropertyDescriptor() |
[[DefineOwnProperty]] |
defineProperty() |
[[HasProperty]] |
has() |
[[Get]] |
get() |
[[Set]] |
set() |
[[Delete]] |
deleteProperty() |
[[OwnPropertyKeys]] |
ownKeys() |
Об'єкти-функції, крім перелічених вище, мають наступні внутрішні методи:
Внутрішній метод | Відповідна пастка |
---|---|
[[Call]] |
apply() |
[[Construct]] |
construct() |
Важливо усвідомити, що всі взаємодії з об'єктом врешті-решт зводяться до заклику одного з цих внутрішніх методів, і що всі вони – налаштовні за допомогою заступників. Це означає, що майже жодна поведінка (окрім певних критичних інваріантів) у мові не гарантована – все визначається самим об'єктом. Коли запустити delete obj.x
, то немає гарантій, що "x" in obj
поверне після цього false
: це залежить від реалізації об'єктом внутрішніх методів [[Delete]]
і [[HasProperty]]
. Зате delete obj.x
може виводити щось у консоль, змінювати якийсь глобальний стан, або навіть визначати якусь нову властивість замість видалення наявної, хоч такої семантики й варто уникати у власному коді.
Усі внутрішні методи викликаються самою мовою, вони недоступні безпосередньо в коді на JavaScript. Простір імен Reflect
надає методи, котрі роблять трохи більше, ніж просто викликають внутрішні методи та проводять трохи нормалізації й валідації вихідних даних. На сторінках кожної з пасток перелічені різні типові ситуації, коли закликається пастка, але ці внутрішні методи викликаються в багатьох місцях. Наприклад, методи масивів зчитують і записують масив крізь такі внутрішні методи, тож методи штибу push()
так само закликають пастки get()
і set()
.
Більшість внутрішніх методів є прямолінійними в тому, що вони роблять. Лише два з них можуть спантеличувати: [[Set]]
і [[DefineOwnProperty]]
. Для звичайних об'єктів перший із них закликає сетери, а другий – ні. (Крім цього, [[Set]]
потайки викликає [[DefineOwnProperty]]
, якщо наявної властивості немає, або якщо властивість є властивістю даних.) Хоч ви можете знати, що синтаксис obj.x = 1
застосовує [[Set]]
, а Object.defineProperty()
застосовує [[DefineOwnProperty]]
, не є цілковито очевидним, яку семантику використовує решта вбудованих методів та записів. Наприклад, класові поля застосовують семантику [[DefineOwnProperty]]
, і саме тому сетери, визначені у надкласі, не закликаються, коли поле оголошується на похідному класі.
Конструктор
Proxy()
Створює новий об'єкт
Proxy
.
Примітка: Властивості
Proxy.prototype
немає, тож примірникиProxy
не мають жодних особливих властивостей або методів.
Статичні методи
Proxy.revocable()
Створює відкличний об'єкт
Proxy
.
Приклади
Базовий приклад
У цьому простому прикладі число 37
повертається як усталене значення, коли імені властивості немає в об'єкті. Він використовує обробник get()
.
const handler = {
get(obj, prop) {
return prop in obj ? obj[prop] : 37;
},
};
const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;
console.log(p.a, p.b); // 1, undefined
console.log("c" in p, p.c); // false, 37
Заступник автоматичного перенаправлення
У цьому прикладі використовується нативний об'єкт JavaScript, до якого наш заступник перенаправлятиме усі операції, що до нього застосовуються.
const target = {};
const p = new Proxy(target, {});
p.a = 37; // Операція перенаправлена на target
console.log(target.a); // 37 (Операція була коректно перенаправлена!)
Зверніть увагу, що хоч таке "автоматичне" перенаправлення працює для простих об'єктів JavaScript, воно не спрацює для нативних об'єктів, як то елементів DOM, об'єктів Map
та всього, що має внутрішні комірки. Докладніше про це в Немає перенаправлення приватних властивостей.
Немає перенаправлення приватних властивостей
Заступник все одно є окремим об'єктом, з окремою ідентичністю – він є заступником, що працює між загорнутим об'єктом і зовнішнім світом. Таким чином, заступник не має безпосереднього доступу до приватних властивостей вихідного об'єкта.
class Secret {
#secret;
constructor(secret) {
this.#secret = secret;
}
get secret() {
return this.#secret.replace(/\d+/, "[ПРИХОВАНО]");
}
}
const aSecret = new Secret("123456");
console.log(aSecret.secret); // [ПРИХОВАНО]
// Схоже на автоматичне перенаправлення...
const proxy = new Proxy(aSecret, {});
console.log(proxy.secret); // TypeError: Cannot read private member #secret from an object whose class did not declare it
Так відбувається через те, що коли на заступнику закликається пастка get
, то значення this
– це proxy
, а не вихідний об'єкт secret
, тож поле #secret
– недоступне. Аби це виправити, необхідно використати вихідний об'єкт secret
за значення this
:
const proxy = new Proxy(aSecret, {
get(target, prop, receiver) {
// Усталено це має вигляд Reflect.get(target, prop, receiver)
// у випадку чого значення `this` – інакше
return target[prop];
},
});
console.log(proxy.secret);
Це означає, що у випадку з методами доведеться так само переспрямувати значення this
методу на вихідний об'єкт:
class Secret {
#x = 1;
x() {
return this.#x;
}
}
const aSecret = new Secret();
const proxy = new Proxy(aSecret, {
get(target, prop, receiver) {
const value = target[prop];
if (value instanceof Function) {
return function (...args) {
return value.apply(this === receiver ? target : this, args);
};
}
return value;
},
});
console.log(proxy.x());
Частина нативних об'єктів JavaScript має властивості, що звуться внутрішніми комірками й не є доступними з коду на JavaScript. Наприклад, об'єкти Map
мають приховану комірку, що зветься [[MapData]]
, в котрій зберігаються пари ключ-значення відображення. Таким чином, не можна тривіально створити заступник перенаправлення для відображення:
const proxy = new Proxy(new Map(), {});
console.log(proxy.size); // TypeError: get size method called on incompatible Proxy
Необхідно використати заступник "з відновленням this
", проілюстрований вище, аби це обійти.
Валідація
За допомогою Proxy
можна легко перевірити на чинність передане об'єктові значення. Цей приклад використовує обробник set()
.
const validator = {
set(obj, prop, value) {
if (prop === "age") {
if (!Number.isInteger(value)) {
throw new TypeError("age не є цілим числом");
}
if (value > 200) {
throw new RangeError("age здається недійсним");
}
}
// Усталена логіка – зберегти значення
obj[prop] = value;
// Повідомити про успіх
return true;
},
};
const person = new Proxy({}, validator);
person.age = 100;
console.log(person.age); // 100
person.age = "юний"; // Викидає виняток
person.age = 300; // Викидає виняток
Маніпуляція вузлами DOM
У цьому прикладі Proxy
вживається для перемикання атрибутів двох окремих елементів, щоб коли атрибут вмикався на одному елементі, він вимикався на іншому.
Створюється об'єкт view
, котрий є заступником для об'єкта зі властивістю selected
. Обробник заступника визначає обробник set()
.
Коли елемент HTML присвоюється в view.selected
, то атрибут 'aria-selected'
цього елемента отримує значення true
. Коли в view.selected
присвоюється інший елемент, то атрибут 'aria-selected'
такого елемента отримує значення true
, а атрибут 'aria-selected'
попереднього елемента – автоматично отримує значення false
.
const view = new Proxy(
{
selected: null,
},
{
set(obj, prop, newval) {
const oldval = obj[prop];
if (prop === "selected") {
if (oldval) {
oldval.setAttribute("aria-selected", "false");
}
if (newval) {
newval.setAttribute("aria-selected", "true");
}
}
// Усталена логіка для збереження значення
obj[prop] = newval;
// Повідомити про успіх
return true;
},
},
);
const item1 = document.getElementById("item-1");
const item2 = document.getElementById("item-2");
// вибір item1:
view.selected = item1;
console.log(`item1: ${item1.getAttribute("aria-selected")}`);
// item1: true
// вибір item2 скасовує вибір item1:
view.selected = item2;
console.log(`item1: ${item1.getAttribute("aria-selected")}`);
// item1: false
console.log(`item2: ${item2.getAttribute("aria-selected")}`);
// item2: true
Коригування значення та додаткова властивість
Об'єкт-заступник products
оцінює передане значення та перетворює його на масив, якщо це потрібно. Також цей об'єкт підтримує додаткову властивість latestBrowser
– має для неї як гетер, так і сетер.
const products = new Proxy(
{
browsers: ["Firefox", "Chrome"],
},
{
get(obj, prop) {
// Додаткова властивість
if (prop === "latestBrowser") {
return obj.browsers[obj.browsers.length - 1];
}
// Усталена логіка для повернення значення
return obj[prop];
},
set(obj, prop, value) {
// Додаткова властивість
if (prop === "latestBrowser") {
obj.browsers.push(value);
return true;
}
// Перетворення значення, коли воно не є масивом
if (typeof value === "string") {
value = [value];
}
// Усталена логіка для збереження значення
obj[prop] = value;
// Повідомити про успіх
return true;
},
},
);
console.log(products.browsers);
// ['Firefox', 'Chrome']
products.browsers = "Safari";
// передати рядок (через помилку)
console.log(products.browsers);
// ['Safari'] <- проблем немає, значення є масивом
products.latestBrowser = "Edge";
console.log(products.browsers);
// ['Safari', 'Edge']
console.log(products.latestBrowser);
// 'Edge'
Приклад з повним списком пасток
Щоб створити зразок з повним списком пасток, для дидактичних потреб, спробуємо заступити ненативний об'єкт, що є особливо годящим для такого роду операцій – глобальний об'єкт docCookies
, створений простим фреймворком реп'яшків.
/*
const docCookies = ... об'єкт "docCookies" взяти тут:
https://reference.codeproject.com/dom/document/cookie/simple_document.cookie_framework
*/
const docCookies = new Proxy(docCookies, {
get(target, key) {
return target[key] ?? target.getItem(key) ?? undefined;
},
set(target, key, value) {
if (key in target) {
return false;
}
return target.setItem(key, value);
},
deleteProperty(target, key) {
if (!(key in target)) {
return false;
}
return target.removeItem(key);
},
ownKeys(target) {
return target.keys();
},
has(target, key) {
return key in target || target.hasItem(key);
},
defineProperty(target, key, descriptor) {
if (descriptor && "value" in descriptor) {
target.setItem(key, descriptor.value);
}
return target;
},
getOwnPropertyDescriptor(target, key) {
const value = target.getItem(key);
return value
? {
value,
writable: true,
enumerable: true,
configurable: false,
}
: undefined;
},
});
/* Перевірка реп'яшків */
console.log((docCookies.myCookie1 = "Перше значення"));
console.log(docCookies.getItem("myCookie1"));
docCookies.setItem("myCookie1", "Змінене значення");
console.log(docCookies.myCookie1);
Специфікації
Сумісність із браузерами
desktop | mobile | server | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Proxy
|
Chrome Full support 49 | Edge Full support 12 | Firefox Full support 18 | Internet Explorer No support Ні | Opera Full support 36 | Safari Full support 10 | WebView Android Full support 49 | Chrome Android Full support 49 | Firefox for Android Full support 18 | Opera Android Full support 36 | Safari on iOS Full support 10 | Samsung Internet Full support 5.0 | Deno Full support 1.0 | Node.js Full support 6.0.0 |
Proxy() constructor
|
Chrome Full support 49 | Edge Full support 12 | Firefox Full support 18 | Internet Explorer No support Ні | Opera Full support 36 | Safari Full support 10 | WebView Android Full support 49 | Chrome Android Full support 49 | Firefox for Android Full support 18 | Opera Android Full support 36 | Safari on iOS Full support 10 | Samsung Internet Full support 5.0 | Deno Full support 1.0 | Node.js Full support 6.0.0 |
revocable
|
Chrome Full support 63 | Edge Full support 12 | Firefox Full support 34 | Internet Explorer No support Ні | Opera Full support 50 | Safari Full support 10 | WebView Android Full support 63 | Chrome Android Full support 63 | Firefox for Android Full support 34 | Opera Android Full support 46 | Safari on iOS Full support 10 | Samsung Internet Full support 8.0 | Deno Full support 1.0 | Node.js Full support 6.0.0 |
Дивіться також
- Презентація Заступники – чудові від Брендана Айка на JSConf (2014)