Visaptverošs ceļvedis globāliem izstrādātājiem JavaScript Proxy API apgūšanai. Iemācieties pārtvert un pielāgot objektu darbības ar praktiskiem piemēriem, lietošanas gadījumiem un veiktspējas padomiem.
JavaScript Proxy API: Dziļš Ieskats Objektu Uzvedības Modifikācijā
Mūsdienu JavaScript mainīgajā ainavā izstrādātāji pastāvīgi meklē spēcīgākus un elegantākus veidus, kā pārvaldīt un mijiedarboties ar datiem. Lai gan tādas funkcijas kā klases, moduļi un async/await ir revolucionizējušas veidu, kā mēs rakstām kodu, ECMAScript 2015 (ES6) tika ieviesta spēcīga metaprogrammēšanas funkcija, kas bieži paliek nepietiekami izmantota: Proxy API.
Metaprogrammēšana var izklausīties biedējoši, bet tas ir vienkārši jēdziens par koda rakstīšanu, kas darbojas ar citu kodu. Proxy API ir JavaScript galvenais rīks šim nolūkam, kas ļauj izveidot "proxy" citam objektam, kas var pārtvert un no jauna definēt šī objekta fundamentālās darbības. Tas ir kā pielāgojama vārtsarga novietošana objekta priekšā, sniedzot jums pilnīgu kontroli pār to, kā tam piekļūst un kā tas tiek modificēts.
Šis visaptverošais ceļvedis demistificēs Proxy API. Mēs izpētīsim tā pamatjēdzienus, sadalīsim tā dažādās iespējas ar praktiskiem piemēriem un apspriedīsim uzlabotus lietošanas gadījumus un veiktspējas apsvērumus. Beigās jūs sapratīsiet, kāpēc Proxies ir mūsdienu ietvaru stūrakmens un kā jūs varat tos izmantot, lai rakstītu tīrāku, spēcīgāku un vieglāk uzturamu kodu.
Pamatjēdzienu izpratne: Mērķis, Apstrādātājs un Slazdi
Proxy API ir balstīts uz trim fundamentāliem komponentiem. Izpratne par to lomām ir atslēga proxy apgūšanai.
- Mērķis: Šis ir oriģinālais objekts, kuru vēlaties ietīt. Tas var būt jebkura veida objekts, ieskaitot masīvus, funkcijas vai pat citu proxy. Proxy virtualizē šo mērķi, un visas darbības galu galā (lai arī ne obligāti) tiek pārsūtītas uz to.
- Apstrādātājs: Šis ir objekts, kas satur proxy loģiku. Tas ir viettura objekts, kura rekvizīti ir funkcijas, kas pazīstamas kā "slazdi". Kad proxy notiek darbība, tas meklē atbilstošu slazdu apstrādātājā.
- Slazdi: Šīs ir metodes apstrādātājā, kas nodrošina piekļuvi rekvizītiem. Katrs slazds atbilst fundamentālai objektu darbībai. Piemēram,
get
slazds pārtver rekvizītu lasīšanu, unset
slazds pārtver rekvizītu rakstīšanu. Ja slazds nav definēts apstrādātājā, darbība vienkārši tiek pārsūtīta uz mērķi tā, it kā proxy nebūtu.
Proxy izveides sintakse ir vienkārša:
const proxy = new Proxy(target, handler);
Apskatīsim ļoti vienkāršu piemēru. Mēs izveidosim proxy, kas vienkārši nodod visas darbības mērķa objektam, izmantojot tukšu apstrādātāju.
// The original object
const target = {
message: "Hello, World!"
};
// An empty handler. All operations will be forwarded to the target.
const handler = {};
// The proxy object
const proxy = new Proxy(target, handler);
// Accessing a property on the proxy
console.log(proxy.message); // Output: Hello, World!
// The operation was forwarded to the target
console.log(target.message); // Output: Hello, World!
// Modifying a property through the proxy
proxy.anotherMessage = "Hello, Proxy!";
console.log(proxy.anotherMessage); // Output: Hello, Proxy!
console.log(target.anotherMessage); // Output: Hello, Proxy!
Šajā piemērā proxy uzvedas tieši tāpat kā oriģinālais objekts. Īstais spēks nāk tad, kad mēs sākam definēt slazdus apstrādātājā.
Proxy anatomija: Izpētot biežāk sastopamos slazdus
Apstrādātāja objekts var saturēt līdz 13 dažādiem slazdiem, katrs no tiem atbilst JavaScript objektu fundamentālai iekšējai metodei. Izpētīsim visbiežāk sastopamos un noderīgos.
Rekvizītu piekļuves slazdi
1. `get(target, property, receiver)`
Šis, iespējams, ir visvairāk izmantotais slazds. Tas tiek aktivizēts, kad tiek nolasīts proxy rekvizīts.
target
: Oriģinālais objekts.property
: Rekvizīta nosaukums, kuram piekļūst.receiver
: Pats proxy vai objekts, kas to manto.
Piemērs: Noklusējuma vērtības neeksistējošiem rekvizītiem.
const user = {
firstName: 'John',
lastName: 'Doe',
age: 30
};
const userHandler = {
get(target, property) {
// If the property exists on the target, return it.
// Otherwise, return a default message.
return property in target ? target[property] : `Property '${property}' does not exist.`;
}
};
const userProxy = new Proxy(user, userHandler);
console.log(userProxy.firstName); // Output: John
console.log(userProxy.age); // Output: 30
console.log(userProxy.country); // Output: Property 'country' does not exist.
2. `set(target, property, value, receiver)`
set
slazds tiek izsaukts, kad proxy rekvizītam tiek piešķirta vērtība. Tas ir lieliski piemērots validācijai, reģistrēšanai vai tikai lasāmu objektu izveidei.
value
: Jaunā vērtība, kas tiek piešķirta rekvizītam.- Slazdam jāatgriež Būla vērtība:
true
, ja piešķiršana bija veiksmīga, unfalse
pretējā gadījumā (kas stingrā režīmā izraisīsTypeError
).
Piemērs: Datu validācija.
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('Age must be an integer.');
}
if (value <= 0) {
throw new RangeError('Age must be a positive number.');
}
}
// If validation passes, set the value on the target object.
target[property] = value;
// Indicate success.
return true;
}
};
const personProxy = new Proxy(person, validationHandler);
personProxy.age = 30; // This is valid
console.log(personProxy.age); // Output: 30
try {
personProxy.age = 'thirty'; // Throws TypeError
} catch (e) {
console.error(e.message); // Output: Age must be an integer.
}
try {
personProxy.age = -5; // Throws RangeError
} catch (e) {
console.error(e.message); // Output: Age must be a positive number.
}
3. `has(target, property)`
Šis slazds pārtver in
operatoru. Tas ļauj jums kontrolēt, kuri rekvizīti šķiet pastāvam objektā.
Piemērs: "Privātu" rekvizītu slēpšana.
JavaScript izplatīta konvencija ir prefiksēt privātos rekvizītus ar pasvītrojumu (_). Mēs varam izmantot has
slazdu, lai tos paslēptu no in
operatora.
const secretData = {
_apiKey: 'xyz123abc',
publicKey: 'pub456def',
id: 1
};
const hidingHandler = {
has(target, property) {
if (property.startsWith('_')) {
return false; // Pretend it doesn't exist
}
return property in target;
}
};
const dataProxy = new Proxy(secretData, hidingHandler);
console.log('publicKey' in dataProxy); // Output: true
console.log('_apiKey' in dataProxy); // Output: false (even though it's on the target)
console.log('id' in dataProxy); // Output: true
Piezīme: Tas ietekmē tikai in
operatoru. Tieša piekļuve, piemēram, dataProxy._apiKey
, joprojām darbotos, ja vien jūs neieviešat arī atbilstošu get
slazdu.
4. `deleteProperty(target, property)`
Šis slazds tiek izpildīts, kad rekvizīts tiek dzēsts, izmantojot delete
operatoru. Tas ir noderīgi, lai novērstu svarīgu rekvizītu dzēšanu.
Slazdam jāatgriež true
, lai dzēšana būtu veiksmīga, vai false
, ja dzēšana neizdevās.
Piemērs: Rekvizītu dzēšanas novēršana.
const immutableConfig = {
databaseUrl: 'prod.db.server',
port: 8080
};
const deletionGuardHandler = {
deleteProperty(target, property) {
if (property in target) {
console.warn(`Attempted to delete protected property: '${property}'. Operation denied.`);
return false;
}
return true; // Property didn't exist anyway
}
};
const configProxy = new Proxy(immutableConfig, deletionGuardHandler);
delete configProxy.port;
// Console output: Attempted to delete protected property: 'port'. Operation denied.
console.log(configProxy.port); // Output: 8080 (It wasn't deleted)
Objektu uzskaitīšanas un apraksta slazdi
5. `ownKeys(target)`
Šo slazdu aktivizē darbības, kas iegūst objekta pašu rekvizītu sarakstu, piemēram, Object.keys()
, Object.getOwnPropertyNames()
, Object.getOwnPropertySymbols()
un Reflect.ownKeys()
.
Piemērs: Atslēgu filtrēšana.
Apvienosim to ar mūsu iepriekšējo "privāto" rekvizītu piemēru, lai tos pilnībā paslēptu.
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) {
// Also prevent direct access
if (property.startsWith('_')) {
return undefined;
}
return Reflect.get(target, property, receiver);
}
};
const fullProxy = new Proxy(secretData, keyHidingHandler);
console.log(Object.keys(fullProxy)); // Output: ['publicKey', 'id']
console.log('publicKey' in fullProxy); // Output: true
console.log('_apiKey' in fullProxy); // Output: false
console.log(fullProxy._apiKey); // Output: undefined
Ievērojiet, ka mēs šeit izmantojam Reflect
. Reflect
objekts nodrošina metodes pārtveramām JavaScript darbībām, un tā metodēm ir tādi paši nosaukumi un paraksti kā proxy slazdiem. Ir laba prakse izmantot Reflect
, lai pārsūtītu oriģinālo darbību uz mērķi, nodrošinot, ka tiek pareizi uzturēta noklusējuma darbība.
Funkciju un konstruktoru slazdi
Proxy neaprobežojas tikai ar vienkāršiem objektiem. Kad mērķis ir funkcija, jūs varat pārtvert izsaukumus un konstrukcijas.
6. `apply(target, thisArg, argumentsList)`
Šis slazds tiek izsaukts, kad tiek izpildīts funkcijas proxy. Tas pārtver funkcijas izsaukumu.
target
: Oriģinālā funkcija.thisArg
:this
konteksts izsaukumam.argumentsList
: Funkcijai nodoto argumentu saraksts.
Piemērs: Funkciju izsaukumu un to argumentu reģistrēšana.
function sum(a, b) {
return a + b;
}
const loggingHandler = {
apply(target, thisArg, argumentsList) {
console.log(`Calling function '${target.name}' with arguments: ${argumentsList}`);
// Execute the original function with the correct context and arguments
const result = Reflect.apply(target, thisArg, argumentsList);
console.log(`Function '${target.name}' returned: ${result}`);
return result;
}
};
const proxiedSum = new Proxy(sum, loggingHandler);
proxiedSum(5, 10);
// Console output:
// Calling function 'sum' with arguments: 5,10
// Function 'sum' returned: 15
7. `construct(target, argumentsList, newTarget)`
Šis slazds pārtver new
operatora izmantošanu klases vai funkcijas proxy.
Piemērs: Singleton modeļa ieviešana.
class MyDatabaseConnection {
constructor(url) {
this.url = url;
console.log(`Connecting to ${this.url}...`);
}
}
let instance;
const singletonHandler = {
construct(target, argumentsList) {
if (!instance) {
console.log('Creating new instance.');
instance = Reflect.construct(target, argumentsList);
}
console.log('Returning existing instance.');
return instance;
}
};
const ProxiedConnection = new Proxy(MyDatabaseConnection, singletonHandler);
const conn1 = new ProxiedConnection('db://primary');
// Console output:
// Creating new instance.
// Connecting to db://primary...
// Returning existing instance.
const conn2 = new ProxiedConnection('db://secondary'); // URL will be ignored
// Console output:
// Returning existing instance.
console.log(conn1 === conn2); // Output: true
console.log(conn1.url); // Output: db://primary
console.log(conn2.url); // Output: db://primary
Praktiski lietošanas gadījumi un uzlaboti modeļi
Tagad, kad esam apskatījuši atsevišķus slazdus, apskatīsim, kā tos var apvienot, lai atrisinātu reālās pasaules problēmas.
1. API abstrakcija un datu transformācija
API bieži atgriež datus formātā, kas neatbilst jūsu lietojumprogrammas konvencijām (piemēram, snake_case
pret camelCase
). Proxy var pārredzami apstrādāt šo konvertēšanu.
function snakeToCamel(s) {
return s.replace(/(_\\w)/g, (m) => m[1].toUpperCase());
}
// Imagine this is our raw data from an API
const apiResponse = {
user_id: 123,
first_name: 'Alice',
last_name: 'Wonderland',
account_status: 'active'
};
const camelCaseHandler = {
get(target, property) {
const camelCaseProperty = snakeToCamel(property);
// Check if the camelCase version exists directly
if (camelCaseProperty in target) {
return target[camelCaseProperty];
}
// Fallback to original property name
if (property in target) {
return target[property];
}
return undefined;
}
};
const userModel = new Proxy(apiResponse, camelCaseHandler);
// We can now access properties using camelCase, even though they are stored as snake_case
console.log(userModel.userId); // Output: 123
console.log(userModel.firstName); // Output: Alice
console.log(userModel.accountStatus); // Output: active
2. Novērojamie un datu piesaistīšana (mūsdienu ietvaru pamatā)
Proxy ir reaktivitātes sistēmu dzinējs tādos mūsdienu ietvaros kā Vue 3. Kad maināt rekvizītu proxy stāvokļa objektā, set
slazdu var izmantot, lai aktivizētu atjauninājumus lietotāja saskarnē vai citās lietojumprogrammas daļās.
Šeit ir ļoti vienkāršots piemērs:
function createObservable(target, callback) {
const handler = {
set(obj, prop, value) {
const result = Reflect.set(obj, prop, value);
callback(prop, value); // Trigger the callback on change
return result;
}
};
return new Proxy(target, handler);
}
const state = {
count: 0,
message: 'Hello'
};
function render(prop, value) {
console.log(`CHANGE DETECTED: The property '${prop}' was set to '${value}'. Re-rendering UI...`);
}
const observableState = createObservable(state, render);
observableState.count = 1;
// Console output: CHANGE DETECTED: The property 'count' was set to '1'. Re-rendering UI...
observableState.message = 'Goodbye';
// Console output: CHANGE DETECTED: The property 'message' was set to 'Goodbye'. Re-rendering UI...
3. Negatīvi masīva indeksi
Klasisks un jautrs piemērs ir vietējo masīvu uzvedības paplašināšana, lai atbalstītu negatīvus indeksus, kur -1
attiecas uz pēdējo elementu, līdzīgi kā tādās valodās kā Python.
function createNegativeArrayProxy(arr) {
const handler = {
get(target, property) {
const index = Number(property);
if (!Number.isNaN(index) && index < 0) {
// Convert negative index to a positive one from the end
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]); // Output: a
console.log(proxiedArray[-1]); // Output: e
console.log(proxiedArray[-2]); // Output: d
console.log(proxiedArray.length); // Output: 5
Veiktspējas apsvērumi un labākā prakse
Lai gan proxy ir neticami spēcīgi, tie nav burvju lode. Ir ļoti svarīgi saprast to ietekmi.
Veiktspējas papildizdevumi
Proxy ievieš novirzīšanas slāni. Katrai darbībai ar proxy objektu jāiziet cauri apstrādātājam, kas pievieno nelielu papildizdevumu salīdzinājumā ar tiešu darbību ar vienkāršu objektu. Lielākajai daļai lietojumprogrammu (piemēram, datu validācijai vai ietvara līmeņa reaktivitātei) šis papildizdevums ir nenozīmīgs. Tomēr veiktspējai kritiskā kodā, piemēram, saspringtā ciklā, kas apstrādā miljoniem vienumu, tas var kļūt par vājā vietu. Vienmēr veiciet etalonu, ja veiktspēja ir galvenā problēma.
Proxy invarianti
Slazds nevar pilnībā melot par mērķa objekta būtību. JavaScript izpilda noteikumu kopumu, ko sauc par "invariantiem", kuriem proxy slazdiem ir jāpakļaujas. Invariantu pārkāpšana izraisīs TypeError
.
Piemēram, deleteProperty
slazda invariants ir tāds, ka tas nevar atgriezt true
(norādot uz panākumiem), ja atbilstošais mērķa objekta rekvizīts nav konfigurējams. Tas neļauj proxy apgalvot, ka tas ir izdzēsis rekvizītu, kuru nevar izdzēst.
const target = {};
Object.defineProperty(target, 'unbreakable', { value: 10, configurable: false });
const handler = {
deleteProperty(target, prop) {
// This will violate the invariant
return true;
}
};
const proxy = new Proxy(target, handler);
try {
delete proxy.unbreakable; // This will throw an error
} catch (e) {
console.error(e.message);
// Output: 'deleteProperty' on proxy: returned true for non-configurable property 'unbreakable'
}
Kad izmantot proxy (un kad nē)
- Labi: Ietvaru un bibliotēku veidošanai (piemēram, stāvokļa pārvaldība, ORM), atkļūdošanai un reģistrēšanai, stabilu validācijas sistēmu ieviešanai un spēcīgu API izveidei, kas abstrahē pamatā esošās datu struktūras.
- Apsveriet alternatīvas: Veiktspējai kritiskiem algoritmiem, vienkāršiem objektu paplašinājumiem, kur pietiktu ar klasi vai rūpnīcas funkciju, vai arī tad, ja jums jāatbalsta ļoti vecas pārlūkprogrammas, kurām nav ES6 atbalsta.
Atsaucami proxy
Scenārijiem, kad jums var būt nepieciešams "izslēgt" proxy (piemēram, drošības apsvērumu vai atmiņas pārvaldības dēļ), JavaScript nodrošina Proxy.revocable()
. Tas atgriež objektu, kas satur gan proxy, gan revoke
funkciju.
const target = { data: 'sensitive' };
const handler = {};
const { proxy, revoke } = Proxy.revocable(target, handler);
console.log(proxy.data); // Output: sensitive
// Now, we revoke the proxy's access
revoke();
try {
console.log(proxy.data); // This will throw an error
} catch (e) {
console.error(e.message);
// Output: Cannot perform 'get' on a proxy that has been revoked
}
Proxy pret citiem metaprogrammēšanas paņēmieniem
Pirms Proxy izstrādātāji izmantoja citas metodes, lai sasniegtu līdzīgus mērķus. Ir noderīgi saprast, kā Proxy salīdzinās.
Object.defineProperty()
Object.defineProperty()
modificē objektu tieši, definējot getterus un setterus noteiktiem rekvizītiem. Savukārt proxy nemodificē oriģinālo objektu vispār; tie to ietin.
- Darbības joma:
defineProperty
darbojas katram rekvizītam atsevišķi. Jums jādefinē getteris/setteris katram rekvizītam, kuru vēlaties skatīties. Proxyget
unset
slazdi ir globāli, pārtverot darbības ar jebkuru rekvizītu, ieskaitot jaunus, kas pievienoti vēlāk. - Iespējas: Proxy var pārtvert plašāku darbību klāstu, piemēram,
deleteProperty
,in
operatoru un funkciju izsaukumus, kodefineProperty
nevar darīt.
Secinājums: Virtualizācijas spēks
JavaScript Proxy API ir vairāk nekā tikai gudra funkcija; tas ir fundamentāls pavērsiens veidā, kā mēs varam projektēt un mijiedarboties ar objektiem. Ļaujot mums pārtvert un pielāgot fundamentālās darbības, Proxy paver durvis spēcīgu modeļu pasaulei: no vienmērīgas datu validācijas un transformācijas līdz reaktīvām sistēmām, kas nodrošina mūsdienīgas lietotāja saskarnes.
Lai gan tiem ir nelielas veiktspējas izmaksas un jāievēro noteikumu kopums, to spēja izveidot tīras, atdalītas un spēcīgas abstrakcijas ir nepārspējama. Virtualizējot objektus, jūs varat izveidot sistēmas, kas ir robustākas, vieglāk uzturamas un izteiksmīgākas. Nākamreiz, kad saskaraties ar sarežģītu izaicinājumu, kas saistīts ar datu pārvaldību, validāciju vai novērojamību, apsveriet, vai Proxy ir pareizais rīks šim darbam. Tas varētu būt elegantākais risinājums jūsu rīkkopā.