Slovenščina

Celovit vodnik za globalne razvijalce o obvladovanju JavaScript Proxy API-ja. Naučite se prestrezati in prilagajati operacije na objektih s praktičnimi primeri, primeri uporabe in nasveti za zmogljivost.

JavaScript Proxy API: Poglobljen vpogled v spreminjanje obnašanja objektov

V razvijajočem se okolju sodobnega JavaScripta razvijalci nenehno iščejo močnejše in elegantnejše načine za upravljanje in interakcijo s podatki. Medtem ko so funkcionalnosti, kot so razredi, moduli in async/await, revolucionirale način pisanja kode, obstaja močna metaprogramirna funkcionalnost, uvedena v ECMAScript 2015 (ES6), ki pogosto ostaja neizkoriščena: Proxy API.

Metaprogramiranje se morda sliši zastrašujoče, vendar je to preprosto koncept pisanja kode, ki deluje na drugi kodi. Proxy API je glavno orodje JavaScripta za to, saj omogoča ustvarjanje 'posrednika' (proxy) za drug objekt, ki lahko prestreže in na novo opredeli temeljne operacije za ta objekt. To je kot postavitev prilagodljivega vratarja pred objekt, kar vam daje popoln nadzor nad tem, kako se do njega dostopa in kako se spreminja.

Ta celovit vodnik bo demistificiral Proxy API. Raziskali bomo njegove osrednje koncepte, razčlenili njegove različne zmožnosti s praktičnimi primeri ter razpravljali o naprednih primerih uporabe in vidikih zmogljivosti. Na koncu boste razumeli, zakaj so posredniki (Proxies) temelj sodobnih ogrodij in kako jih lahko izkoristite za pisanje čistejše, močnejše in lažje vzdrževane kode.

Razumevanje osnovnih konceptov: Cilj, upravljalnik in pasti

Proxy API temelji na treh osnovnih komponentah. Razumevanje njihovih vlog je ključ do obvladovanja posrednikov.

Sintaksa za ustvarjanje posrednika je preprosta:

const proxy = new Proxy(target, handler);

Poglejmo si zelo osnoven primer. Ustvarili bomo posrednika, ki preprosto posreduje vse operacije ciljnemu objektu z uporabo praznega upravljalnika.


// Izvirni objekt
const target = {
  message: "Hello, World!"
};

// Prazen upravljalnik. Vse operacije bodo posredovane cilju.
const handler = {};

// Objekt posrednika
const proxy = new Proxy(target, handler);

// Dostopanje do lastnosti na posredniku
console.log(proxy.message); // Izhod: Hello, World!

// Operacija je bila posredovana cilju
console.log(target.message); // Izhod: Hello, World!

// Spreminjanje lastnosti prek posrednika
proxy.anotherMessage = "Hello, Proxy!";

console.log(proxy.anotherMessage); // Izhod: Hello, Proxy!
console.log(target.anotherMessage); // Izhod: Hello, Proxy!

V tem primeru se posrednik obnaša popolnoma enako kot izvirni objekt. Prava moč se pokaže, ko začnemo definiranje pasti v upravljalniku.

Anatomija posrednika: Raziskovanje pogostih pasti

Objekt upravljalnika lahko vsebuje do 13 različnih pasti, od katerih vsaka ustreza temeljni interni metodi objektov v JavaScriptu. Raziščimo najpogostejše in najbolj uporabne.

Pasti za dostop do lastnosti

1. `get(target, property, receiver)`

To je verjetno najpogosteje uporabljena past. Sproži se, ko se prebere lastnost posrednika.

Primer: Privzete vrednosti za neobstoječe lastnosti.


const user = {
  firstName: 'John',
  lastName: 'Doe',
  age: 30
};

const userHandler = {
  get(target, property) {
    // Če lastnost obstaja na cilju, jo vrni.
    // V nasprotnem primeru vrni privzeto sporočilo.
    return property in target ? target[property] : `Lastnost '${property}' ne obstaja.`;
  }
};

const userProxy = new Proxy(user, userHandler);

console.log(userProxy.firstName); // Izhod: John
console.log(userProxy.age);       // Izhod: 30
console.log(userProxy.country);   // Izhod: Lastnost 'country' ne obstaja.

2. `set(target, property, value, receiver)`

Past set se pokliče, ko se lastnosti posrednika dodeli vrednost. Je popolna za preverjanje veljavnosti, beleženje ali ustvarjanje objektov samo za branje.

Primer: Preverjanje veljavnosti podatkov.


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('Starost mora biti celo število.');
      }
      if (value <= 0) {
        throw new RangeError('Starost mora biti pozitivno število.');
      }
    }

    // Če preverjanje uspe, nastavi vrednost na ciljnem objektu.
    target[property] = value;

    // Označi uspeh.
    return true;
  }
};

const personProxy = new Proxy(person, validationHandler);

personProxy.age = 30; // To je veljavno
console.log(personProxy.age); // Izhod: 30

try {
  personProxy.age = 'thirty'; // Vrne TypeError
} catch (e) {
  console.error(e.message); // Izhod: Starost mora biti celo število.
}

try {
  personProxy.age = -5; // Vrne RangeError
} catch (e) {
  console.error(e.message); // Izhod: Starost mora biti pozitivno število.
}

3. `has(target, property)`

Ta past prestreže operator in. Omogoča vam nadzor nad tem, katere lastnosti se zdijo, da obstajajo na objektu.

Primer: Skrivanje 'zasebnih' lastnosti.

V JavaScriptu je običajna konvencija, da se zasebne lastnosti označijo s podčrtajem (_). Past has lahko uporabimo, da jih skrijemo pred operatorjem in.


const secretData = {
  _apiKey: 'xyz123abc',
  publicKey: 'pub456def',
  id: 1
};

const hidingHandler = {
  has(target, property) {
    if (property.startsWith('_')) {
      return false; // Pretvarjaj se, da ne obstaja
    }
    return property in target;
  }
};

const dataProxy = new Proxy(secretData, hidingHandler);

console.log('publicKey' in dataProxy); // Izhod: true
console.log('_apiKey' in dataProxy);   // Izhod: false (čeprav je na cilju)
console.log('id' in dataProxy);        // Izhod: true

Opomba: To vpliva samo na operator in. Neposreden dostop, kot je dataProxy._apiKey, bi še vedno deloval, razen če implementirate tudi ustrezno past get.

4. `deleteProperty(target, property)`

Ta past se izvede, ko se lastnost izbriše z operatorjem delete. Uporabna je za preprečevanje brisanja pomembnih lastnosti.

Past mora vrniti true za uspešno brisanje ali false za neuspešno.

Primer: Preprečevanje brisanja lastnosti.


const immutableConfig = {
  databaseUrl: 'prod.db.server',
  port: 8080
};

const deletionGuardHandler = {
  deleteProperty(target, property) {
    if (property in target) {
      console.warn(`Poskus brisanja zaščitene lastnosti: '${property}'. Operacija zavrnjena.`);
      return false;
    }
    return true; // Lastnost tako ali tako ni obstajala
  }
};

const configProxy = new Proxy(immutableConfig, deletionGuardHandler);

delete configProxy.port;
// Izhod v konzoli: Poskus brisanja zaščitene lastnosti: 'port'. Operacija zavrnjena.

console.log(configProxy.port); // Izhod: 8080 (Ni bila izbrisana)

Pasti za naštevanje in opisovanje objektov

5. `ownKeys(target)`

Ta past se sproži pri operacijah, ki pridobijo seznam lastnih lastnosti objekta, kot so Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols() in Reflect.ownKeys().

Primer: Filtriranje ključev.

Združimo to z našim prejšnjim primerom 'zasebnih' lastnosti, da jih popolnoma skrijemo.


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) {
    // Prepreči tudi neposreden dostop
    if (property.startsWith('_')) {
      return undefined;
    }
    return Reflect.get(target, property, receiver);
  }
};

const fullProxy = new Proxy(secretData, keyHidingHandler);

console.log(Object.keys(fullProxy)); // Izhod: ['publicKey', 'id']
console.log('publicKey' in fullProxy); // Izhod: true
console.log('_apiKey' in fullProxy);   // Izhod: false
console.log(fullProxy._apiKey);      // Izhod: undefined

Opazite, da tukaj uporabljamo Reflect. Objekt Reflect ponuja metode za prestrezljive operacije v JavaScriptu, njegove metode pa imajo enaka imena in podpise kot pasti posrednika. Uporaba Reflect za posredovanje izvirne operacije cilju je najboljša praksa, ki zagotavlja pravilno ohranjanje privzetega obnašanja.

Pasti za funkcije in konstruktorje

Posredniki niso omejeni na navadne objekte. Ko je cilj funkcija, lahko prestrežete klice in konstrukcije.

6. `apply(target, thisArg, argumentsList)`

Ta past se pokliče, ko se izvede posrednik funkcije. Prestreže klic funkcije.

Primer: Beleženje klicev funkcij in njihovih argumentov.


function sum(a, b) {
  return a + b;
}

const loggingHandler = {
  apply(target, thisArg, argumentsList) {
    console.log(`Klicanje funkcije '${target.name}' z argumenti: ${argumentsList}`);
    // Izvedi izvirno funkcijo s pravilnim kontekstom in argumenti
    const result = Reflect.apply(target, thisArg, argumentsList);
    console.log(`Funkcija '${target.name}' je vrnila: ${result}`);
    return result;
  }
};

const proxiedSum = new Proxy(sum, loggingHandler);

proxiedSum(5, 10);
// Izhod v konzoli:
// Klicanje funkcije 'sum' z argumenti: 5,10
// Funkcija 'sum' je vrnila: 15

7. `construct(target, argumentsList, newTarget)`

Ta past prestreže uporabo operatorja new na posredniku razreda ali funkcije.

Primer: Implementacija vzorca Singleton.


class MyDatabaseConnection {
  constructor(url) {
    this.url = url;
    console.log(`Povezovanje z ${this.url}...`);
  }
}

let instance;

const singletonHandler = {
  construct(target, argumentsList) {
    if (!instance) {
      console.log('Ustvarjanje nove instance.');
      instance = Reflect.construct(target, argumentsList);
    }
    console.log('Vračanje obstoječe instance.');
    return instance;
  }
};

const ProxiedConnection = new Proxy(MyDatabaseConnection, singletonHandler);

const conn1 = new ProxiedConnection('db://primary');
// Izhod v konzoli:
// Ustvarjanje nove instance.
// Povezovanje z db://primary...
// Vračanje obstoječe instance.

const conn2 = new ProxiedConnection('db://secondary'); // URL bo prezrt
// Izhod v konzoli:
// Vračanje obstoječe instance.

console.log(conn1 === conn2); // Izhod: true
console.log(conn1.url); // Izhod: db://primary
console.log(conn2.url); // Izhod: db://primary

Praktični primeri uporabe in napredni vzorci

Zdaj, ko smo obdelali posamezne pasti, poglejmo, kako jih lahko združimo za reševanje resničnih problemov.

1. Abstraktcija API-ja in transformacija podatkov

API-ji pogosto vračajo podatke v formatu, ki se ne ujema s konvencijami vaše aplikacije (npr. snake_case proti camelCase). Posrednik lahko to pretvorbo opravi transparentno.


function snakeToCamel(s) {
  return s.replace(/(_\w)/g, (m) => m[1].toUpperCase());
}

// Predstavljajte si, da so to naši surovi podatki iz API-ja
const apiResponse = {
  user_id: 123,
  first_name: 'Alice',
  last_name: 'Wonderland',
  account_status: 'active'
};

const camelCaseHandler = {
  get(target, property) {
    const camelCaseProperty = snakeToCamel(property);
    // Preveri, ali različica camelCase obstaja neposredno
    if (camelCaseProperty in target) {
      return target[camelCaseProperty];
    }
    // Zateci se k izvirnemu imenu lastnosti
    if (property in target) {
      return target[property];
    }
    return undefined;
  }
};

const userModel = new Proxy(apiResponse, camelCaseHandler);

// Zdaj lahko dostopamo do lastnosti z uporabo camelCase, čeprav so shranjene kot snake_case
console.log(userModel.userId);        // Izhod: 123
console.log(userModel.firstName);     // Izhod: Alice
console.log(userModel.accountStatus); // Izhod: active

2. Opazovalci (Observables) in vezava podatkov (jedro sodobnih ogrodij)

Posredniki so motor za reaktivnimi sistemi v sodobnih ogrodjih, kot je Vue 3. Ko spremenite lastnost na posredniškem objektu stanja, se lahko past set uporabi za sprožitev posodobitev v uporabniškem vmesniku ali drugih delih aplikacije.

Tukaj je zelo poenostavljen primer:


function createObservable(target, callback) {
  const handler = {
    set(obj, prop, value) {
      const result = Reflect.set(obj, prop, value);
      callback(prop, value); // Ob spremembi sproži povratni klic
      return result;
    }
  };
  return new Proxy(target, handler);
}

const state = {
  count: 0,
  message: 'Hello'
};

function render(prop, value) {
  console.log(`ZAZNANA SPREMEMBA: Lastnost '${prop}' je bila nastavljena na '${value}'. Ponovno izrisovanje UI...`);
}

const observableState = createObservable(state, render);

observableState.count = 1;
// Izhod v konzoli: ZAZNANA SPREMEMBA: Lastnost 'count' je bila nastavljena na '1'. Ponovno izrisovanje UI...

observableState.message = 'Goodbye';
// Izhod v konzoli: ZAZNANA SPREMEMBA: Lastnost 'message' je bila nastavljena na 'Goodbye'. Ponovno izrisovanje UI...

3. Negativni indeksi tabel

Klasičen in zabaven primer je razširitev obnašanja nativnih tabel za podporo negativnim indeksom, kjer -1 pomeni zadnji element, podobno kot v jezikih, kot je Python.


function createNegativeArrayProxy(arr) {
  const handler = {
    get(target, property) {
      const index = Number(property);
      if (!Number.isNaN(index) && index < 0) {
        // Pretvori negativni indeks v pozitivnega od konca
        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]);  // Izhod: a
console.log(proxiedArray[-1]); // Izhod: e
console.log(proxiedArray[-2]); // Izhod: d
console.log(proxiedArray.length); // Izhod: 5

Premisleki o zmogljivosti in najboljše prakse

Čeprav so posredniki izjemno močni, niso čarobna rešitev. Ključnega pomena je razumeti njihove posledice.

Dodatna obremenitev zmogljivosti

Posrednik uvaja plast posredovanja. Vsaka operacija na posredniškem objektu mora iti skozi upravljalnik, kar doda majhno količino dodatne obremenitve v primerjavi z neposredno operacijo na navadnem objektu. Za večino aplikacij (kot je preverjanje veljavnosti podatkov ali reaktivnost na ravni ogrodja) je ta obremenitev zanemarljiva. Vendar pa lahko v kodi, ki je kritična za zmogljivost, kot je tesna zanka, ki obdeluje milijone elementov, to postane ozko grlo. Vedno izvedite primerjalne teste (benchmark), če je zmogljivost primarna skrb.

Invariante posrednikov

Past ne more popolnoma lagati o naravi ciljnega objekta. JavaScript uveljavlja nabor pravil, imenovanih 'invariante', ki jih morajo pasti posrednikov upoštevati. Kršitev invariante bo povzročila TypeError.

Na primer, invarianta za past deleteProperty je, da ne more vrniti true (kar pomeni uspeh), če ustrezna lastnost na ciljnem objektu ni nastavljiva (non-configurable). To preprečuje, da bi posrednik trdil, da je izbrisal lastnost, ki je ni mogoče izbrisati.


const target = {};
Object.defineProperty(target, 'unbreakable', { value: 10, configurable: false });

const handler = {
  deleteProperty(target, prop) {
    // To bo kršilo invarianto
    return true;
  }
};

const proxy = new Proxy(target, handler);

try {
  delete proxy.unbreakable; // To bo vrglo napako
} catch (e) {
  console.error(e.message);
  // Izhod: 'deleteProperty' on proxy: returned true for non-configurable property 'unbreakable'
}

Kdaj uporabiti posrednike (in kdaj ne)

Preklicljivi posredniki

Za scenarije, kjer boste morda morali 'izklopiti' posrednika (npr. iz varnostnih razlogov ali upravljanja pomnilnika), JavaScript ponuja Proxy.revocable(). Ta vrne objekt, ki vsebuje tako posrednika kot funkcijo revoke.


const target = { data: 'sensitive' };
const handler = {};

const { proxy, revoke } = Proxy.revocable(target, handler);

console.log(proxy.data); // Izhod: sensitive

// Zdaj prekličemo dostop posrednika
revoke();

try {
  console.log(proxy.data); // To bo vrglo napako
} catch (e) {
  console.error(e.message);
  // Izhod: Cannot perform 'get' on a proxy that has been revoked
}

Primerjava posrednikov z drugimi tehnikami metaprogramiranja

Pred posredniki so razvijalci za doseganje podobnih ciljev uporabljali druge metode. Koristno je razumeti, kako se posredniki primerjajo z njimi.

`Object.defineProperty()`

Object.defineProperty() neposredno spreminja objekt z definiranjem metod get in set za določene lastnosti. Posredniki pa izvirnega objekta sploh ne spreminjajo; ovijejo ga.

Zaključek: Moč virtualizacije

JavaScript Proxy API je več kot le pametna funkcionalnost; je temeljna sprememba v načinu, kako lahko oblikujemo in komuniciramo z objekti. S tem, ko nam omogočajo prestrezanje in prilagajanje temeljnih operacij, posredniki odpirajo vrata v svet močnih vzorcev: od brezšivnega preverjanja in transformacije podatkov do reaktivnih sistemov, ki poganjajo sodobne uporabniške vmesnike.

Čeprav prinašajo majhno ceno v zmogljivosti in nabor pravil, ki jih je treba upoštevati, je njihova sposobnost ustvarjanja čistih, ločenih in močnih abstrakcij neprimerljiva. Z virtualizacijo objektov lahko gradite sisteme, ki so bolj robustni, vzdržljivi in izrazni. Naslednjič, ko se soočite s kompleksnim izzivom, ki vključuje upravljanje podatkov, preverjanje veljavnosti ali opazovanje, razmislite, ali je posrednik pravo orodje za delo. Morda bo to najelegantnejša rešitev v vaši zbirki orodij.