Komplexní průvodce pro globální vývojáře k zvládnutí JavaScript Proxy API. Naučte se zachytávat a upravovat operace s objekty pomocí praktických příkladů, případů použití a tipů pro výkon.
JavaScript Proxy API: Hloubkový ponor do modifikace chování objektů
V neustále se vyvíjejícím prostředí moderního JavaScriptu vývojáři neustále hledají výkonnější a elegantnější způsoby správy a interakce s daty. Zatímco funkce jako třídy, moduly a async/await způsobily revoluci ve způsobu, jakým píšeme kód, existuje výkonná funkce metaprogramování zavedená v ECMAScript 2015 (ES6), která často zůstává nedostatečně využívaná: Proxy API.
Metaprogramování může znít zastrašujícím způsobem, ale je to jednoduše koncept psaní kódu, který pracuje s jiným kódem. Proxy API je primární nástroj JavaScriptu pro toto, který vám umožňuje vytvořit 'proxy' pro jiný objekt, který může zachytit a předefinovat základní operace pro daný objekt. Je to jako umístit přizpůsobitelného vrátného před objekt, což vám dává úplnou kontrolu nad tím, jak se k němu přistupuje a jak je upravován.
Tento komplexní průvodce objasní Proxy API. Prozkoumáme jeho základní koncepty, rozebereme jeho různé schopnosti s praktickými příklady a prodiskutujeme pokročilé případy použití a úvahy o výkonu. Na konci pochopíte, proč jsou Proxies základním kamenem moderních frameworků a jak je můžete využít k psaní čistšího, výkonnějšího a udržitelnějšího kódu.
Pochopení základních konceptů: Target, Handler a Traps
Proxy API je postaveno na třech základních komponentách. Pochopení jejich rolí je klíčem k zvládnutí proxies.
- Target: Toto je původní objekt, který chcete obalit. Může to být jakýkoli druh objektu, včetně polí, funkcí nebo dokonce jiného proxy. Proxy virtualizuje tento cíl a všechny operace jsou nakonec (i když ne nutně) předány do něj.
- Handler: Toto je objekt, který obsahuje logiku pro proxy. Je to zástupný objekt, jehož vlastnosti jsou funkce, známé jako 'traps'. Když dojde k operaci na proxy, hledá odpovídající trap na handleru.
- Traps: Toto jsou metody na handleru, které poskytují přístup k vlastnostem. Každý trap odpovídá základní operaci objektu. Například trap
get
zachycuje čtení vlastnosti a trapset
zachycuje zápis vlastnosti. Pokud trap není definován na handleru, operace je jednoduše předána do cíle, jako by proxy neexistovalo.
Syntaxe pro vytvoření proxy je přímočará:
const proxy = new Proxy(target, handler);
Podívejme se na velmi jednoduchý příklad. Vytvoříme proxy, které jednoduše předává všechny operace do cílového objektu pomocí prázdného handleru.
// Původní objekt
const target = {
message: "Hello, World!"
};
// Prázdný handler. Všechny operace budou předány do cíle.
const handler = {};
// Proxy objekt
const proxy = new Proxy(target, handler);
// Přístup k vlastnosti na proxy
console.log(proxy.message); // Výstup: Hello, World!
// Operace byla předána do cíle
console.log(target.message); // Výstup: Hello, World!
// Úprava vlastnosti prostřednictvím proxy
proxy.anotherMessage = "Hello, Proxy!";
console.log(proxy.anotherMessage); // Výstup: Hello, Proxy!
console.log(target.anotherMessage); // Výstup: Hello, Proxy!
V tomto příkladu se proxy chová přesně jako původní objekt. Skutečná síla přichází, když začneme definovat traps v handleru.
Anatomie Proxy: Prozkoumání běžných Traps
Objekt handleru může obsahovat až 13 různých traps, z nichž každý odpovídá základní interní metodě objektů JavaScriptu. Prozkoumejme nejběžnější a nejužitečnější.
Traps pro přístup k vlastnostem
1. `get(target, property, receiver)`
Toto je pravděpodobně nejpoužívanější trap. Spouští se, když je čtena vlastnost proxy.
target
: Původní objekt.property
: Název přistupované vlastnosti.receiver
: Samotné proxy nebo objekt, který z něj dědí.
Příklad: Výchozí hodnoty pro neexistující vlastnosti.
const user = {
firstName: 'John',
lastName: 'Doe',
age: 30
};
const userHandler = {
get(target, property) {
// Pokud vlastnost existuje na targetu, vraťte ji.
// Jinak vraťte výchozí zprávu.
return property in target ? target[property] : `Vlastnost '${property}' neexistuje.`;
}
};
const userProxy = new Proxy(user, userHandler);
console.log(userProxy.firstName); // Výstup: John
console.log(userProxy.age); // Výstup: 30
console.log(userProxy.country); // Výstup: Vlastnost 'country' neexistuje.
2. `set(target, property, value, receiver)`
Trap set
je volán, když je vlastnosti proxy přiřazena hodnota. Je ideální pro validaci, protokolování nebo vytváření objektů jen pro čtení.
value
: Nová hodnota přiřazovaná vlastnosti.- Trap musí vrátit boolean:
true
, pokud bylo přiřazení úspěšné, afalse
jinak (což v striktním režimu vyvoláTypeError
).
Příklad: Validace dat.
const person = {
name: 'Jane Doe',
age: 25
};
const validationHandler = {
set(target, property, value) {
if (property === 'age') {
if (typeof value !== 'number' || !Number.isInteger(value)) {
throw new TypeError('Věk musí být celé číslo.');
}
if (value <= 0) {
throw new RangeError('Věk musí být kladné číslo.');
}
}
// Pokud validace projde, nastavte hodnotu na target objektu.
target[property] = value;
// Označte úspěch.
return true;
}
};
const personProxy = new Proxy(person, validationHandler);
personProxy.age = 30; // Toto je platné
console.log(personProxy.age); // Výstup: 30
try {
personProxy.age = 'thirty'; // Vyvolá TypeError
} catch (e) {
console.error(e.message); // Výstup: Věk musí být celé číslo.
}
try {
personProxy.age = -5; // Vyvolá RangeError
} catch (e) {
console.error(e.message); // Výstup: Věk musí být kladné číslo.
}
3. `has(target, property)`
Tento trap zachycuje operátor in
. Umožňuje vám kontrolovat, které vlastnosti se zdají existovat na objektu.
Příklad: Skrytí 'soukromých' vlastností.
V JavaScriptu je běžnou konvencí předpona soukromých vlastností podtržítkem (_). Můžeme použít trap has
k jejich skrytí před operátorem in
.
const secretData = {
_apiKey: 'xyz123abc',
publicKey: 'pub456def',
id: 1
};
const hidingHandler = {
has(target, property) {
if (property.startsWith('_')) {
return false; // Předstírejte, že neexistuje
}
return property in target;
}
};
const dataProxy = new Proxy(secretData, hidingHandler);
console.log('publicKey' in dataProxy); // Výstup: true
console.log('_apiKey' in dataProxy); // Výstup: false (i když je na targetu)
console.log('id' in dataProxy); // Výstup: true
Poznámka: Toto ovlivňuje pouze operátor in
. Přímý přístup jako dataProxy._apiKey
by stále fungoval, pokud také neimplementujete odpovídající trap get
.
4. `deleteProperty(target, property)`
Tento trap je spuštěn, když je vlastnost smazána pomocí operátoru delete
. Je užitečný pro zabránění smazání důležitých vlastností.
Trap musí vrátit true
pro úspěšné smazání nebo false
pro neúspěšné.
Příklad: Zabránění smazání vlastností.
const immutableConfig = {
databaseUrl: 'prod.db.server',
port: 8080
};
const deletionGuardHandler = {
deleteProperty(target, property) {
if (property in target) {
console.warn(`Pokus o smazání chráněné vlastnosti: '${property}'. Operace zamítnuta.`);
return false;
}
return true; // Vlastnost stejně neexistovala
}
};
const configProxy = new Proxy(immutableConfig, deletionGuardHandler);
delete configProxy.port;
// Výstup konzole: Pokus o smazání chráněné vlastnosti: 'port'. Operace zamítnuta.
console.log(configProxy.port); // Výstup: 8080 (Nebyla smazána)
Traps pro výčet a popis objektů
5. `ownKeys(target)`
Tento trap je spuštěn operacemi, které získají seznam vlastních vlastností objektu, jako jsou Object.keys()
, Object.getOwnPropertyNames()
, Object.getOwnPropertySymbols()
a Reflect.ownKeys()
.
Příklad: Filtrování klíčů.
Pojďme to zkombinovat s naším předchozím příkladem 'soukromé' vlastnosti, abychom je plně skryli.
const secretData = {
_apiKey: 'xyz123abc',
publicKey: 'pub456def',
id: 1
};
const keyHidingHandler = {
has(target, property) {
return !property.startsWith('_') && property in target;
},
ownKeys(target) {
return Reflect.ownKeys(target).filter(key => !key.startsWith('_'));
},
get(target, property, receiver) {
// Zabraňte také přímému přístupu
if (property.startsWith('_')) {
return undefined;
}
return Reflect.get(target, property, receiver);
}
};
const fullProxy = new Proxy(secretData, keyHidingHandler);
console.log(Object.keys(fullProxy)); // Výstup: ['publicKey', 'id']
console.log('publicKey' in fullProxy); // Výstup: true
console.log('_apiKey' in fullProxy); // Výstup: false
console.log(fullProxy._apiKey); // Výstup: undefined
Všimněte si, že zde používáme Reflect
. Objekt Reflect
poskytuje metody pro zachytitelné operace JavaScriptu a jeho metody mají stejné názvy a podpisy jako proxy traps. Je osvědčeným postupem používat Reflect
k předání původní operace do cíle, což zajišťuje správné zachování výchozího chování.
Traps pro funkce a konstruktory
Proxies se neomezují pouze na prosté objekty. Když je cílem funkce, můžete zachytit volání a konstrukce.
6. `apply(target, thisArg, argumentsList)`
Tento trap je volán, když je spuštěno proxy funkce. Zachycuje volání funkce.
target
: Původní funkce.thisArg
: Kontextthis
pro volání.argumentsList
: Seznam argumentů předaných funkci.
Příklad: Protokolování volání funkcí a jejich argumentů.
function sum(a, b) {
return a + b;
}
const loggingHandler = {
apply(target, thisArg, argumentsList) {
console.log(`Volání funkce '${target.name}' s argumenty: ${argumentsList}`);
// Spusťte původní funkci se správným kontextem a argumenty
const result = Reflect.apply(target, thisArg, argumentsList);
console.log(`Funkce '${target.name}' vrátila: ${result}`);
return result;
}
};
const proxiedSum = new Proxy(sum, loggingHandler);
proxiedSum(5, 10);
// Výstup konzole:
// Volání funkce 'sum' s argumenty: 5,10
// Funkce 'sum' vrátila: 15
7. `construct(target, argumentsList, newTarget)`
Tento trap zachycuje použití operátoru new
na proxy třídy nebo funkce.
Příklad: Implementace vzoru Singleton.
class MyDatabaseConnection {
constructor(url) {
this.url = url;
console.log(`Připojování k ${this.url}...`);
}
}
let instance;
const singletonHandler = {
construct(target, argumentsList) {
if (!instance) {
console.log('Vytváření nové instance.');
instance = Reflect.construct(target, argumentsList);
}
console.log('Vracení existující instance.');
return instance;
}
};
const ProxiedConnection = new Proxy(MyDatabaseConnection, singletonHandler);
const conn1 = new ProxiedConnection('db://primary');
// Výstup konzole:
// Vytváření nové instance.
// Připojování k db://primary...
// Vracení existující instance.
const conn2 = new ProxiedConnection('db://secondary'); // URL bude ignorována
// Výstup konzole:
// Vracení existující instance.
console.log(conn1 === conn2); // Výstup: true
console.log(conn1.url); // Výstup: db://primary
console.log(conn2.url); // Výstup: db://primary
Praktické případy použití a pokročilé vzory
Nyní, když jsme probrali jednotlivé traps, podívejme se, jak je lze kombinovat k řešení problémů reálného světa.
1. Abstrakce API a transformace dat
API často vracejí data ve formátu, který neodpovídá konvencím vaší aplikace (např. snake_case
vs. camelCase
). Proxy může transparentně zpracovat tuto konverzi.
function snakeToCamel(s) {
return s.replace(/(_\w)/g, (m) => m[1].toUpperCase());
}
// Představte si, že toto jsou naše surová data z API
const apiResponse = {
user_id: 123,
first_name: 'Alice',
last_name: 'Wonderland',
account_status: 'active'
};
const camelCaseHandler = {
get(target, property) {
const camelCaseProperty = snakeToCamel(property);
// Zkontrolujte, zda verze camelCase existuje přímo
if (camelCaseProperty in target) {
return target[camelCaseProperty];
}
// Vraťte se k původnímu názvu vlastnosti
if (property in target) {
return target[property];
}
return undefined;
}
};
const userModel = new Proxy(apiResponse, camelCaseHandler);
// Nyní můžeme přistupovat k vlastnostem pomocí camelCase, i když jsou uloženy jako snake_case
console.log(userModel.userId); // Výstup: 123
console.log(userModel.firstName); // Výstup: Alice
console.log(userModel.accountStatus); // Výstup: active
2. Observables a Data Binding (Jádro moderních frameworků)
Proxies jsou motorem reaktivních systémů v moderních frameworkech, jako je Vue 3. Když změníte vlastnost na proxied stavovém objektu, trap set
lze použít ke spuštění aktualizací v uživatelském rozhraní nebo jiných částech aplikace.
Zde je vysoce zjednodušený příklad:
function createObservable(target, callback) {
const handler = {
set(obj, prop, value) {
const result = Reflect.set(obj, prop, value);
callback(prop, value); // Spusťte zpětné volání při změně
return result;
}
};
return new Proxy(target, handler);
}
const state = {
count: 0,
message: 'Hello'
};
function render(prop, value) {
console.log(`DETEKOVÁNA ZMĚNA: Vlastnost '${prop}' byla nastavena na '${value}'. Opětovné vykreslování UI...`);
}
const observableState = createObservable(state, render);
observableState.count = 1;
// Výstup konzole: DETEKOVÁNA ZMĚNA: Vlastnost 'count' byla nastavena na '1'. Opětovné vykreslování UI...
observableState.message = 'Goodbye';
// Výstup konzole: DETEKOVÁNA ZMĚNA: Vlastnost 'message' byla nastavena na 'Goodbye'. Opětovné vykreslování UI...
3. Záporné indexy polí
Klasickým a zábavným příkladem je rozšíření nativního chování polí pro podporu záporných indexů, kde -1
odkazuje na poslední prvek, podobně jako v jazycích jako Python.
function createNegativeArrayProxy(arr) {
const handler = {
get(target, property) {
const index = Number(property);
if (!Number.isNaN(index) && index < 0) {
// Převeďte záporný index na kladný od konce
property = String(target.length + index);
}
return Reflect.get(target, property);
}
};
return new Proxy(arr, handler);
}
const originalArray = ['a', 'b', 'c', 'd', 'e'];
const proxiedArray = createNegativeArrayProxy(originalArray);
console.log(proxiedArray[0]); // Výstup: a
console.log(proxiedArray[-1]); // Výstup: e
console.log(proxiedArray[-2]); // Výstup: d
console.log(proxiedArray.length); // Výstup: 5
Úvahy o výkonu a osvědčené postupy
I když jsou proxies neuvěřitelně výkonné, nejsou zázračnou kulkou. Je důležité porozumět jejich důsledkům.
Režie výkonu
Proxy zavádí vrstvu nepřímého přístupu. Každá operace na proxied objektu musí projít handlerem, což přidává malé množství režie ve srovnání s přímou operací na prostém objektu. U většiny aplikací (jako je validace dat nebo reaktivita na úrovni frameworku) je tato režie zanedbatelná. Nicméně v kódu kritickém pro výkon, jako je smyčka zpracovávající miliony položek, se to může stát úzkým hrdlem. Vždy provádějte benchmark, pokud je výkon primárním problémem.
Proxy Invariants
Trap nemůže zcela lhát o povaze cílového objektu. JavaScript vynucuje sadu pravidel nazývaných 'invariants', které proxy traps musí dodržovat. Porušení invariant způsobí TypeError
.
Například invariant pro trap deleteProperty
je, že nemůže vrátit true
(označující úspěch), pokud odpovídající vlastnost na cílovém objektu není nekonfigurovatelná. To zabraňuje proxy tvrdit, že smazala vlastnost, kterou nelze smazat.
const target = {};
Object.defineProperty(target, 'unbreakable', { value: 10, configurable: false });
const handler = {
deleteProperty(target, prop) {
// Toto poruší invariant
return true;
}
};
const proxy = new Proxy(target, handler);
try {
delete proxy.unbreakable; // Toto vyvolá chybu
} catch (e) {
console.error(e.message);
// Výstup: 'deleteProperty' na proxy: vrátil true pro nekonfigurovatelnou vlastnost 'unbreakable'
}
Kdy používat Proxies (a kdy ne)
- Vhodné pro: Budování frameworků a knihoven (např. správa stavu, ORM), ladění a protokolování, implementace robustních validačních systémů a vytváření výkonných API, která abstrahují základní datové struktury.
- Zvažte alternativy pro: Algoritmy kritické pro výkon, jednoduchá rozšíření objektů, kde by stačila třída nebo tovární funkce, nebo když potřebujete podporovat velmi staré prohlížeče, které nemají podporu ES6.
Odvolatelné Proxies
Pro scénáře, kde možná budete muset 'vypnout' proxy (např. z bezpečnostních důvodů nebo správy paměti), JavaScript poskytuje Proxy.revocable()
. Vrátí objekt obsahující jak proxy, tak funkci revoke
.
const target = { data: 'sensitive' };
const handler = {};
const { proxy, revoke } = Proxy.revocable(target, handler);
console.log(proxy.data); // Výstup: sensitive
// Nyní odvoláme přístup proxy
revoke();
try {
console.log(proxy.data); // Toto vyvolá chybu
} catch (e) {
console.error(e.message);
// Výstup: Nelze provést 'get' na proxy, které bylo odvoláno
}
Proxies vs. Jiné techniky metaprogramování
Před Proxies vývojáři používali jiné metody k dosažení podobných cílů. Je užitečné porozumět tomu, jak se Proxies srovnávají.`Object.defineProperty()`
Object.defineProperty()
upravuje objekt přímo definováním getters a setters pro konkrétní vlastnosti. Proxies na druhou stranu nemění původní objekt vůbec; obalují ho.
- Rozsah:
defineProperty
funguje na bázi každé vlastnosti. Musíte definovat getter/setter pro každou vlastnost, kterou chcete sledovat. Trapsget
aset
Proxy jsou globální a zachycují operace na jakékoli vlastnosti, včetně nových přidaných později. - Schopnosti: Proxies mohou zachytit širší škálu operací, jako jsou
deleteProperty
, operátorin
a volání funkcí, kterédefineProperty
nemůže dělat.
Závěr: Síla virtualizace
JavaScript Proxy API je více než jen chytrá funkce; je to zásadní posun ve způsobu, jakým můžeme navrhovat a interagovat s objekty. Tím, že nám umožňuje zachytit a přizpůsobit základní operace, Proxies otevírají dveře do světa výkonných vzorů: od bezproblémové validace a transformace dat po reaktivní systémy, které pohánějí moderní uživatelská rozhraní.
I když přicházejí s malými náklady na výkon a sadou pravidel, která je třeba dodržovat, jejich schopnost vytvářet čisté, oddělené a výkonné abstrakce je bezkonkurenční. Virtualizací objektů můžete budovat systémy, které jsou robustnější, udržitelnější a expresivnější. Až se příště setkáte se složitou výzvou týkající se správy dat, validace nebo pozorovatelnosti, zvažte, zda je Proxy tím správným nástrojem pro danou práci. Může to být nejelegantnější řešení ve vaší sadě nástrojů.