Išsamus vadovas pasauliniams kūrėjams, kaip įvaldyti JavaScript Proxy API. Išmokite perimti ir pritaikyti objektų operacijas su praktiniais pavyzdžiais, naudojimo atvejais ir našumo patarimais.
JavaScript Proxy API: Išsamus Objektų Elgsenos Modifikavimo Tyrimas
Šiuolaikinio JavaScript besivystančiame kraštovaizdyje kūrėjai nuolat ieško galingesnių ir elegantiškesnių būdų valdyti ir sąveikauti su duomenimis. Nors tokios funkcijos kaip klasės, moduliai ir async/await sukėlė revoliuciją, kaip rašome kodą, ECMAScript 2015 (ES6) pristatyta galinga metaprogramavimo funkcija, kuri dažnai lieka nepakankamai panaudota: Proxy API.
Metaprogramavimas gali skambėti bauginančiai, bet tai tiesiog kodo, kuris veikia su kitu kodu, rašymo koncepcija. Proxy API yra pagrindinis JavaScript įrankis tam, leidžiantis sukurti „proxy“ kitam objektui, kuris gali perimti ir iš naujo apibrėžti pagrindines to objekto operacijas. Tai tarsi pritaikomas vartininkas prieš objektą, suteikiantis jums visišką kontrolę, kaip jis pasiekiamas ir modifikuojamas.
Šis išsamus vadovas demistifikuos Proxy API. Ištirsime jo pagrindines sąvokas, suskaidysime įvairias jo galimybes su praktiniais pavyzdžiais ir aptarsime pažangius naudojimo atvejus bei našumo aspektus. Galiausiai suprasite, kodėl Proxy yra šiuolaikinių sistemų kertinis akmuo ir kaip galite juos panaudoti rašydami švaresnį, galingesnį ir lengviau prižiūrimą kodą.
Pagrindinių sąvokų supratimas: Taikinys, Tvarkytuvas ir Spąstai
Proxy API yra sukurtas iš trijų pagrindinių komponentų. Jų vaidmenų supratimas yra raktas į proxy įvaldymą.
- Taikinys: Tai originalus objektas, kurį norite apvynioti. Tai gali būti bet kokio tipo objektas, įskaitant masyvus, funkcijas ar net kitą proxy. Proxy virtualizuoja šį taikinį, ir visos operacijos galiausiai (nors nebūtinai) persiunčiamos jam.
- Tvarkytuvas: Tai objektas, kuriame yra proxy logika. Tai vietos rezervavimo objektas, kurio savybės yra funkcijos, vadinamos „spąstais“. Kai proxy vykdoma operacija, ji ieško atitinkamo spąsto tvarkytuve.
- Spąstai: Tai tvarkytuvo metodai, kurie suteikia prieigą prie nuosavybės. Kiekvienas spąstas atitinka pagrindinę objekto operaciją. Pavyzdžiui,
get
spąstas perima nuosavybės skaitymą, oset
spąstas perima nuosavybės rašymą. Jei tvarkytuve spąstas neapibrėžtas, operacija tiesiog persiunčiama į taikinį, tarsi proxy nebūtų.
Proxy kūrimo sintaksė yra paprasta:
const proxy = new Proxy(target, handler);
Pažvelkime į labai paprastą pavyzdį. Sukursime proxy, kuris tiesiog persiunčia visas operacijas per taikinio objektą, naudodami tuščią tvarkytuvą.
// Originalus objektas
const target = {
message: "Sveikas, Pasauli!
};
// Tuščias tvarkytuvas. Visos operacijos bus persiųstos į taikinį.
const handler = {};
// Proxy objektas
const proxy = new Proxy(target, handler);
// Prieiga prie nuosavybės proxy
console.log(proxy.message); // Išvestis: Sveikas, Pasauli!
// Operacija buvo persiųsta į taikinį
console.log(target.message); // Išvestis: Sveikas, Pasauli!
// Nuosavybės modifikavimas per proxy
proxy.anotherMessage = "Sveikas, Proxy!";
console.log(proxy.anotherMessage); // Išvestis: Sveikas, Proxy!
console.log(target.anotherMessage); // Išvestis: Sveikas, Proxy!
Šiame pavyzdyje proxy elgiasi lygiai taip pat, kaip ir originalus objektas. Tikroji galia atsiranda, kai pradedame apibrėžti spąstus tvarkytuve.
Proxy anatomija: bendrų spąstų tyrinėjimas
Tvarkytuvo objektas gali turėti iki 13 skirtingų spąstų, kurių kiekvienas atitinka pagrindinį vidinį JavaScript objektų metodą. Panagrinėkime dažniausiai naudojamus ir naudingiausius.
Nuosavybės prieigos spąstai
1. get(target, property, receiver)
Tai, be jokios abejonės, dažniausiai naudojamas spąstas. Jis suaktyvinamas, kai skaitoma proxy nuosavybė.
target
: Originalus objektas.property
: Pasiekiamos nuosavybės pavadinimas.receiver
: Pats proxy arba objektas, kuris paveldi iš jo.
Pavyzdys: Numatytosios reikšmės neegzistuojančioms nuosavybėms.
const user = {
firstName: 'John',
lastName: 'Doe',
age: 30
};
const userHandler = {
get(target, property) {
// Jei nuosavybė egzistuoja taikinyje, grąžinkite ją.
// Priešingu atveju grąžinkite numatytąjį pranešimą.
return property in target ? target[property] : `Nuosavybė '${property}' neegzistuoja.`;
}
};
const userProxy = new Proxy(user, userHandler);
console.log(userProxy.firstName); // Išvestis: John
console.log(userProxy.age); // Išvestis: 30
console.log(userProxy.country); // Išvestis: Nuosavybė 'country' neegzistuoja.
2. set(target, property, value, receiver)
set
spąstas iškviečiamas, kai proxy nuosavybei priskiriama reikšmė. Tai puikiai tinka patvirtinimui, registravimui ar tik skaitymui skirtų objektų kūrimui.
value
: Nauja reikšmė, priskiriama nuosavybei.- Spąstas turi grąžinti boolean:
true
, jei priskyrimas buvo sėkmingas, irfalse
kitu atveju (kuris griežtuoju režimu išmesTypeError
).
Pavyzdys: Duomenų patvirtinimas.
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('Amžius turi būti sveikasis skaičius.');
}
if (value <= 0) {
throw new RangeError('Amžius turi būti teigiamas skaičius.');
}
}
// Jei patvirtinimas sėkmingas, nustatykite reikšmę taikinio objekte.
target[property] = value;
// Nurodykite sėkmę.
return true;
}
};
const personProxy = new Proxy(person, validationHandler);
personProxy.age = 30; // Tai galioja
console.log(personProxy.age); // Išvestis: 30
try {
personProxy.age = 'thirty'; // Išmeta TypeError
} catch (e) {
console.error(e.message); // Išvestis: Amžius turi būti sveikasis skaičius.
}
try {
personProxy.age = -5; // Išmeta RangeError
} catch (e) {
console.error(e.message); // Išvestis: Amžius turi būti teigiamas skaičius.
}
3. has(target, property)
Šis spąstas perima in
operatorių. Tai leidžia jums kontroliuoti, kurios nuosavybės atrodo esančios objekte.
Pavyzdys: „Privačių“ nuosavybių slėpimas.
JavaScript kalboje įprasta privačioms nuosavybėms suteikti priešdėlį pabraukimo ženklu (_). Mes galime panaudoti has
spąstą, kad paslėptume jas nuo in
operatoriaus.
const secretData = {
_apiKey: 'xyz123abc',
publicKey: 'pub456def',
id: 1
};
const hidingHandler = {
has(target, property) {
if (property.startsWith('_')) {
return false; // Apsimeskite, kad jo nėra
}
return property in target;
}
};
const dataProxy = new Proxy(secretData, hidingHandler);
console.log('publicKey' in dataProxy); // Išvestis: true
console.log('_apiKey' in dataProxy); // Išvestis: false (nors jis yra taikinyje)
console.log('id' in dataProxy); // Išvestis: true
Pastaba: Tai veikia tik su in
operatoriumi. Tiesioginis priėjimas, pavyzdžiui, dataProxy._apiKey
vis tiek veiktų, nebent taip pat įdiegtumėte atitinkamą get
spąstą.
4. deleteProperty(target, property)
Šis spąstas vykdomas, kai nuosavybė ištrinama naudojant delete
operatorių. Tai naudinga norint užkirsti kelią svarbių nuosavybių ištrynimui.
Spąstas turi grąžinti true
sėkmingam ištrynimui arba false
nesėkmingam.
Pavyzdys: Nuosavybių ištrynimo prevencija.
const immutableConfig = {
databaseUrl: 'prod.db.server',
port: 8080
};
const deletionGuardHandler = {
deleteProperty(target, property) {
if (property in target) {
console.warn(`Bandyta ištrinti saugomą nuosavybę: '${property}'. Operacija atmesta.`);
return false;
}
return true; // Nuosavybės vis tiek nebuvo
}
};
const configProxy = new Proxy(immutableConfig, deletionGuardHandler);
delete configProxy.port;
// Konsolės išvestis: Bandyta ištrinti saugomą nuosavybę: 'port'. Operacija atmesta.
console.log(configProxy.port); // Išvestis: 8080 (Jis nebuvo ištrintas)
Objekto išvardijimo ir aprašymo spąstai
5. ownKeys(target)
Šis spąstas suaktyvinamas operacijomis, kurios gauna objekto nuosavų nuosavybių sąrašą, pavyzdžiui, Object.keys()
, Object.getOwnPropertyNames()
, Object.getOwnPropertySymbols()
ir Reflect.ownKeys()
.
Pavyzdys: Raktų filtravimas.
Sujunkime tai su ankstesniu „privačios“ nuosavybės pavyzdžiu, kad visiškai jas paslėptume.
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) {
// Taip pat užkirsti kelią tiesioginiam priėjimui
if (property.startsWith('_')) {
return undefined;
}
return Reflect.get(target, property, receiver);
}
};
const fullProxy = new Proxy(secretData, keyHidingHandler);
console.log(Object.keys(fullProxy)); // Išvestis: ['publicKey', 'id']
console.log('publicKey' in fullProxy); // Išvestis: true
console.log('_apiKey' in fullProxy); // Išvestis: false
console.log(fullProxy._apiKey); // Išvestis: undefined
Atkreipkite dėmesį, kad čia naudojame Reflect
. Reflect
objektas pateikia metodus perimtinoms JavaScript operacijoms, ir jo metodai turi tuos pačius pavadinimus ir parašus kaip ir proxy spąstai. Geriausia praktika yra naudoti Reflect
, norint persiųsti pradinę operaciją į taikinį, užtikrinant teisingą numatytąjį elgesį.
Funkcijos ir konstruktoriaus spąstai
Proxy neapsiriboja paprastais objektais. Kai taikinys yra funkcija, galite perimti iškvietimus ir konstrukcijas.
6. apply(target, thisArg, argumentsList)
Šis spąstas iškviečiamas, kai vykdomas funkcijos proxy. Jis perima funkcijos iškvietimą.
target
: Originali funkcija.thisArg
:this
kontekstas iškvietimui.argumentsList
: Argumentų, perduotų funkcijai, sąrašas.
Pavyzdys: Funkcijų iškvietimų ir jų argumentų registravimas.
function sum(a, b) {
return a + b;
}
const loggingHandler = {
apply(target, thisArg, argumentsList) {
console.log(`Kviečiama funkcija '${target.name}' su argumentais: ${argumentsList}`);
// Vykdykite pradinę funkciją su teisingu kontekstu ir argumentais
const result = Reflect.apply(target, thisArg, argumentsList);
console.log(`Funkcija '${target.name}' grąžino: ${result}`);
return result;
}
};
const proxiedSum = new Proxy(sum, loggingHandler);
proxiedSum(5, 10);
// Konsolės išvestis:
// Kviečiama funkcija 'sum' su argumentais: 5,10
// Funkcija 'sum' grąžino: 15
7. construct(target, argumentsList, newTarget)
Šis spąstas perima new
operatoriaus naudojimą klasės arba funkcijos proxy.
Pavyzdys: Vienišosios (Singleton) šablono įgyvendinimas.
class MyDatabaseConnection {
constructor(url) {
this.url = url;
console.log(`Prisijungiama prie ${this.url}...`);
}
}
let instance;
const singletonHandler = {
construct(target, argumentsList) {
if (!instance) {
console.log('Kuriamas naujas egzempliorius.');
instance = Reflect.construct(target, argumentsList);
}
console.log('Grąžinamas esamas egzempliorius.');
return instance;
}
};
const ProxiedConnection = new Proxy(MyDatabaseConnection, singletonHandler);
const conn1 = new ProxiedConnection('db://primary');
// Konsolės išvestis:
// Kuriamas naujas egzempliorius.
// Prisijungiama prie db://primary...
// Grąžinamas esamas egzempliorius.
const conn2 = new ProxiedConnection('db://secondary'); // URL bus ignoruojamas
// Konsolės išvestis:
// Grąžinamas esamas egzempliorius.
console.log(conn1 === conn2); // Išvestis: true
console.log(conn1.url); // Išvestis: db://primary
console.log(conn2.url); // Išvestis: db://primary
Praktiniai naudojimo atvejai ir pažangūs šablonai
Dabar, kai apžvelgėme atskirus spąstus, pažiūrėkime, kaip juos galima derinti sprendžiant realaus pasaulio problemas.
1. API abstrakcija ir duomenų transformacija
API dažnai grąžina duomenis formatu, kuris neatitinka jūsų programos konvencijų (pvz., snake_case
vs. camelCase
). Proxy gali skaidriai valdyti šį konvertavimą.
function snakeToCamel(s) {
return s.replace(/(_\w)/g, (m) => m[1].toUpperCase());
}
// Įsivaizduokite, kad tai yra mūsų neapdoroti duomenys iš API
const apiResponse = {
user_id: 123,
first_name: 'Alice',
last_name: 'Wonderland',
account_status: 'active'
};
const camelCaseHandler = {
get(target, property) {
const camelCaseProperty = snakeToCamel(property);
// Patikrinkite, ar camelCase versija egzistuoja tiesiogiai
if (camelCaseProperty in target) {
return target[camelCaseProperty];
}
// Atsarginis variantas į originalų nuosavybės pavadinimą
if (property in target) {
return target[property];
}
return undefined;
}
};
const userModel = new Proxy(apiResponse, camelCaseHandler);
// Dabar galime pasiekti nuosavybes naudodami camelCase, net jei jos saugomos kaip snake_case
console.log(userModel.userId); // Išvestis: 123
console.log(userModel.firstName); // Išvestis: Alice
console.log(userModel.accountStatus); // Išvestis: active
2. Stebėjimo objektai ir duomenų susiejimas (Šiuolaikinių sistemų pagrindas)
Proxy yra variklis, esantis už reaktyviųjų sistemų šiuolaikinėse sistemose, pvz., Vue 3. Kai pakeičiate nuosavybę proxied būsenos objekte, set
spąstas gali būti naudojamas norint suaktyvinti atnaujinimus vartotojo sąsajoje ar kitose programos dalyse.
Čia pateikiamas labai supaprastintas pavyzdys:
function createObservable(target, callback) {
const handler = {
set(obj, prop, value) {
const result = Reflect.set(obj, prop, value);
callback(prop, value); // Suaktyvinkite atgalinį ryšį pasikeitus
return result;
}
};
return new Proxy(target, handler);
}
const state = {
count: 0,
message: 'Sveiki'
};
function render(prop, value) {
console.log(`PAKEITIMAS APTIKTAS: Nuosavybė '${prop}' buvo nustatyta į '${value}'. Iš naujo atvaizduojama vartotojo sąsaja...`);
}
const observableState = createObservable(state, render);
observableState.count = 1;
// Konsolės išvestis: PAKEITIMAS APTIKTAS: Nuosavybė 'count' buvo nustatyta į '1'. Iš naujo atvaizduojama vartotojo sąsaja...
observableState.message = 'Sudie';
// Konsolės išvestis: PAKEITIMAS APTIKTAS: Nuosavybė 'message' buvo nustatyta į 'Sudie'. Iš naujo atvaizduojama vartotojo sąsaja...
3. Neigiami masyvo indeksai
Klasikinis ir linksmas pavyzdys yra pradinio masyvo elgesio išplėtimas, kad būtų palaikomi neigiami indeksai, kur -1
reiškia paskutinį elementą, panašiai kaip tokiose kalbose kaip Python.
function createNegativeArrayProxy(arr) {
const handler = {
get(target, property) {
const index = Number(property);
if (!Number.isNaN(index) && index < 0) {
// Konvertuokite neigiamą indeksą į teigiamą iš galo
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]); // Išvestis: a
console.log(proxiedArray[-1]); // Išvestis: e
console.log(proxiedArray[-2]); // Išvestis: d
console.log(proxiedArray.length); // Išvestis: 5
Našumo aspektai ir geriausia praktika
Nors proxy yra nepaprastai galingi, jie nėra magiškas kulkas. Svarbu suprasti jų pasekmes.
Našumo režimas
Proxy įveda netiesioginio sluoksnį. Kiekviena operacija su proxied objektu turi praeiti per tvarkytuvą, o tai prideda nedidelį režimą, palyginti su tiesiogine operacija su paprastu objektu. Daugumoje programų (pvz., duomenų patvirtinimas arba sisteminio lygio reaktyvumas) šis režimas yra nereikšmingas. Tačiau našumui kritiniame kode, pvz., griežtame cikle, apdorojančiame milijonus elementų, tai gali tapti kliūtimi. Visada pamatuokite, jei našumas yra pagrindinis rūpestis.
Proxy invariantai
Spąstas negali visiškai meluoti apie taikinio objekto prigimtį. JavaScript įgyvendina taisyklių rinkinį, vadinamą „invariantais“, kurių proxy spąstai turi paklusti. Invariantų pažeidimas sukels TypeError
.
Pavyzdžiui, deleteProperty
spąsto invariantas yra tas, kad jis negali grąžinti true
(nurodant sėkmę), jei atitinkama nuosavybė taikinyje yra nekonfigūruojama. Tai neleidžia proxy tvirtinti, kad ji ištrynė nuosavybę, kurios negalima ištrinti.
const target = {};
Object.defineProperty(target, 'unbreakable', { value: 10, configurable: false });
const handler = {
deleteProperty(target, prop) {
// Tai pažeis invariantą
return true;
}
};
const proxy = new Proxy(target, handler);
try {
delete proxy.unbreakable; // Tai išmes klaidą
} catch (e) {
console.error(e.message);
// Išvestis: 'deleteProperty' on proxy: returned true for non-configurable property 'unbreakable'
}
Kada naudoti proxy (ir kada ne)
- Gerai: Sistemų ir bibliotekų (pvz., būsenos valdymas, ORM) kūrimas, derinimas ir registravimas, patikimų patvirtinimo sistemų įgyvendinimas ir galingų API, kurios abstrahuoja pagrindines duomenų struktūras, kūrimas.
- Apsvarstykite alternatyvas: Našumui kritiniai algoritmai, paprasti objektų išplėtimai, kai pakaktų klasės ar gamybos funkcijos, arba kai reikia palaikyti labai senas naršykles, kuriose nėra ES6 palaikymo.
Atšaukiami proxy
Scenarijams, kai jums gali prireikti „išjungti“ proxy (pvz., dėl saugumo priežasčių ar atminties valdymo), JavaScript pateikia Proxy.revocable()
. Jis grąžina objektą, kuriame yra ir proxy, ir revoke
funkcija.
const target = { data: 'sensitive' };
const handler = {};
const { proxy, revoke } = Proxy.revocable(target, handler);
console.log(proxy.data); // Išvestis: sensitive
// Dabar atšaukiame proxy prieigą
revoke();
try {
console.log(proxy.data); // Tai išmes klaidą
} catch (e) {
console.error(e.message);
// Išvestis: Cannot perform 'get' on a proxy that has been revoked
}
Proxy vs. Kitos metaprogramavimo technikos
Prieš Proxy kūrėjai naudojo kitus metodus panašiems tikslams pasiekti. Naudinga suprasti, kaip Proxy lyginami.
Object.defineProperty()
Object.defineProperty()
tiesiogiai modifikuoja objektą, apibrėždamas getterius ir setterius konkrečioms nuosavybėms. Kita vertus, Proxy visiškai nemodifikuoja originalaus objekto; jie jį apvynioja.
- Apimtis:
defineProperty
veikia kiekvienos nuosavybės pagrindu. Turite apibrėžti getterį/setterį kiekvienai nuosavybei, kurią norite stebėti. Proxyget
irset
spąstai yra globalūs, užfiksuojantys operacijas su bet kuria nuosavybe, įskaitant naujas, pridėtas vėliau. - Galimybės: Proxy gali perimti platesnį operacijų diapazoną, pvz.,
deleteProperty
,in
operatorių ir funkcijos iškvietimus, kodefineProperty
negali padaryti.
Išvada: Virtualizacijos galia
JavaScript Proxy API yra daugiau nei tik išmani funkcija; tai esminis poslinkis, kaip galime projektuoti ir sąveikauti su objektais. Leidę perimti ir pritaikyti pagrindines operacijas, Proxy atveria duris į galingų šablonų pasaulį: nuo sklandaus duomenų patvirtinimo ir transformacijos iki reaktyviųjų sistemų, kurios maitina šiuolaikines vartotojo sąsajas.
Nors jie turi nedidelę našumo kainą ir taisyklių rinkinį, kurio reikia laikytis, jų gebėjimas kurti švarias, atskirtas ir galingas abstrakcijas yra neprilygstamas. Virtualizuodami objektus, galite kurti sistemas, kurios yra tvirtesnės, lengviau prižiūrimos ir išraiškingesnės. Kitą kartą, kai susidursite su sudėtingu iššūkiu, susijusiu su duomenų valdymu, patvirtinimu ar stebėjimu, pagalvokite, ar Proxy yra tinkamas įrankis darbui atlikti. Tai gali būti pats elegantiškiausias sprendimas jūsų įrankių rinkinyje.