Hrvatski

Sveobuhvatan vodič za globalne programere o ovladavanju JavaScript Proxy API-jem. Naučite presretati i prilagođavati operacije objekata pomoću praktičnih primjera, slučajeva upotrebe i savjeta za performanse.

JavaScript Proxy API: Detaljno Upoznavanje s Modifikacijom Ponašanja Objekata

U evoluirajućem krajoliku modernog JavaScripta, programeri neprestano traže moćnije i elegantnije načine za upravljanje i interakciju s podacima. Iako su značajke poput klasa, modula i async/await revolucionarizirale način na koji pišemo kod, postoji moćna metaprogramska značajka uvedena u ECMAScript 2015 (ES6) koja često ostaje nedovoljno iskorištena: Proxy API.

Metaprogramiranje možda zvuči zastrašujuće, ali to je jednostavno koncept pisanja koda koji radi na drugom kodu. Proxy API je JavaScriptov primarni alat za to, omogućujući vam stvaranje 'proxyja' za drugi objekt, koji može presretati i redefinirati temeljne operacije za taj objekt. To je kao da postavljate prilagodljivog vratara ispred objekta, dajući vam potpunu kontrolu nad načinom pristupa i izmjene.

Ovaj sveobuhvatan vodič će demistificirati Proxy API. Istražit ćemo njegove temeljne koncepte, razložiti njegove različite mogućnosti s praktičnim primjerima i raspravljati o naprednim slučajevima upotrebe i razmatranjima performansi. Do kraja ćete razumjeti zašto su Proxiji kamen temeljac modernih okvira i kako ih možete iskoristiti za pisanje čišćeg, moćnijeg i lakšeg za održavanje koda.

Razumijevanje Temeljnih Koncepta: Cilj, Rukovatelj i Zamke

Proxy API je izgrađen na tri temeljne komponente. Razumijevanje njihovih uloga ključno je za ovladavanje proxijima.

Sintaksa za stvaranje proxyja je jednostavna:

const proxy = new Proxy(target, handler);

Pogledajmo vrlo jednostavan primjer. Stvorit ćemo proxy koji jednostavno prosljeđuje sve operacije ciljnom objektu pomoću praznog rukovatelja.


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

// Prazan rukovatelj. Sve operacije bit će proslijeđene cilju.
const handler = {};

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

// Pristup svojstvu na proxyju
console.log(proxy.message); // Ispis: Hello, World!

// Operacija je proslijeđena cilju
console.log(target.message); // Ispis: Hello, World!

// Izmjena svojstva putem proxyja
proxy.anotherMessage = "Hello, Proxy!";

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

U ovom primjeru, proxy se ponaša točno kao izvorni objekt. Prava snaga dolazi kada počnemo definirati zamke u rukovatelju.

Anatomija Proxyja: Istraživanje Uobičajenih Zamki

Objekt rukovatelja može sadržavati do 13 različitih zamki, od kojih svaka odgovara temeljnoj internoj metodi JavaScript objekata. Istražimo najčešće i najkorisnije.

Zamke Pristupa Svojstvima

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

Ovo je vjerojatno najčešće korištena zamka. Pokreće se kada se pročita svojstvo proxyja.

Primjer: Zadani podaci za nepostojeća svojstva.


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

const userHandler = {
  get(target, property) {
    // Ako svojstvo postoji na cilju, vrati ga.
    // Inače, vrati zadanu poruku.
    return property in target ? target[property] : `Svojstvo '${property}' ne postoji.`;
  }
};

const userProxy = new Proxy(user, userHandler);

console.log(userProxy.firstName); // Ispis: John
console.log(userProxy.age);       // Ispis: 30
console.log(userProxy.country);   // Ispis: Svojstvo 'country' ne postoji.

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

Zamka set se poziva kada se svojstvu proxyja dodijeli vrijednost. Savršena je za validaciju, bilježenje ili stvaranje objekata samo za čitanje.

Primjer: Validacija podataka.


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('Dob mora biti cijeli broj.');
      }
      if (value <= 0) {
        throw new RangeError('Dob mora biti pozitivan broj.');
      }
    }

    // Ako validacija prođe, postavi vrijednost na ciljni objekt.
    target[property] = value;

    // Označite uspjeh.
    return true;
  }
};

const personProxy = new Proxy(person, validationHandler);

personProxy.age = 30; // Ovo je valjano
console.log(personProxy.age); // Ispis: 30

try {
  personProxy.age = 'thirty'; // Baca TypeError
} catch (e) {
  console.error(e.message); // Ispis: Dob mora biti cijeli broj.
}

try {
  personProxy.age = -5; // Baca RangeError
} catch (e) {
  console.error(e.message); // Ispis: Dob mora biti pozitivan broj.
}

3. `has(target, property)`

Ova zamka presreće operator in. Omogućuje vam kontrolu koja se svojstva pojavljuju kao da postoje na objektu.

Primjer: Skrivanje 'privatnih' svojstava.

U JavaScriptu je uobičajena konvencija prefiksirati privatna svojstva podvlakom (_). Možemo koristiti zamku has da ih sakrijemo od operatora in.


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

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

const dataProxy = new Proxy(secretData, hidingHandler);

console.log('publicKey' in dataProxy); // Ispis: true
console.log('_apiKey' in dataProxy);   // Ispis: false (čak i ako je na cilju)
console.log('id' in dataProxy);        // Ispis: true

Napomena: Ovo utječe samo na operator in. Izravan pristup poput dataProxy._apiKey bi i dalje funkcionirao, osim ako također ne implementirate odgovarajuću zamku get.

4. `deleteProperty(target, property)`

Ova se zamka izvršava kada se svojstvo izbriše pomoću operatora delete. Korisno je za sprječavanje brisanja važnih svojstava.

Zamka mora vratiti true za uspješno brisanje ili false za neuspješno.

Primjer: Sprječavanje brisanja svojstava.


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

const deletionGuardHandler = {
  deleteProperty(target, property) {
    if (property in target) {
      console.warn(`Pokušaj brisanja zaštićenog svojstva: '${property}'. Operacija odbijena.`);
      return false;
    }
    return true; // Svojstvo ionako nije postojalo
  }
};

const configProxy = new Proxy(immutableConfig, deletionGuardHandler);

delete configProxy.port;
// Ispis konzole: Pokušaj brisanja zaštićenog svojstva: 'port'. Operacija odbijena.

console.log(configProxy.port); // Ispis: 8080 (Nije izbrisano)

Zamke za Enumeraciju i Opis Objekata

5. `ownKeys(target)`

Ova se zamka pokreće operacijama koje dohvaćaju popis vlastitih svojstava objekta, kao što su Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols() i Reflect.ownKeys().

Primjer: Filtriranje ključeva.

Kombinirajmo ovo s našim prethodnim primjerom 'privatnog' svojstva kako bismo ih u potpunosti sakrili.


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) {
    // Također spriječite izravan pristup
    if (property.startsWith('_')) {
      return undefined;
    }
    return Reflect.get(target, property, receiver);
  }
};

const fullProxy = new Proxy(secretData, keyHidingHandler);

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

Primijetite da ovdje koristimo Reflect. Objekt Reflect pruža metode za operacije JavaScripta koje se mogu presresti, a njegove metode imaju ista imena i potpise kao i zamke proxyja. Najbolja je praksa koristiti Reflect za prosljeđivanje izvorne operacije cilju, osiguravajući da se zadano ponašanje ispravno održava.

Funkcijske i Konstruktorske Zamke

Proxiji nisu ograničeni na obične objekte. Kada je cilj funkcija, možete presresti pozive i konstrukcije.

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

Ova se zamka poziva kada se izvrši proxy funkcije. Presreće poziv funkcije.

Primjer: Bilježenje poziva funkcija i njihovih argumenata.


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

const loggingHandler = {
  apply(target, thisArg, argumentsList) {
    console.log(`Pozivanje funkcije '${target.name}' s argumentima: ${argumentsList}`);
    // Izvršite izvornu funkciju s ispravnim kontekstom i argumentima
    const result = Reflect.apply(target, thisArg, argumentsList);
    console.log(`Funkcija '${target.name}' vratila je: ${result}`);
    return result;
  }
};

const proxiedSum = new Proxy(sum, loggingHandler);

proxiedSum(5, 10);
// Ispis konzole:
// Pozivanje funkcije 'sum' s argumentima: 5,10
// Funkcija 'sum' vratila je: 15

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

Ova zamka presreće upotrebu operatora new na proxyju klase ili funkcije.

Primjer: Implementacija singleton uzorka.


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

let instance;

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

const ProxiedConnection = new Proxy(MyDatabaseConnection, singletonHandler);

const conn1 = new ProxiedConnection('db://primary');
// Ispis konzole:
// Stvaranje nove instance.
// Povezivanje na db://primary...
// Vraćanje postojeće instance.

const conn2 = new ProxiedConnection('db://secondary'); // URL će biti ignoriran
// Ispis konzole:
// Vraćanje postojeće instance.

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

Praktični Slučajevi Upotrebe i Napredni Uzorci

Sada kada smo pokrili pojedinačne zamke, pogledajmo kako se mogu kombinirati za rješavanje problema iz stvarnog svijeta.

1. Apstrakcija API-ja i Transformacija Podataka

API-ji često vraćaju podatke u formatu koji ne odgovara konvencijama vaše aplikacije (npr. snake_case nasuprot camelCase). Proxy može transparentno rukovati ovom konverzijom.


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

// Zamislite da su ovo naši sirovi podaci 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);
    // Provjerite postoji li camelCase verzija izravno
    if (camelCaseProperty in target) {
      return target[camelCaseProperty];
    }
    // Povratak na izvorni naziv svojstva
    if (property in target) {
      return target[property];
    }
    return undefined;
  }
};

const userModel = new Proxy(apiResponse, camelCaseHandler);

// Sada možemo pristupiti svojstvima koristeći camelCase, iako su pohranjena kao snake_case
console.log(userModel.userId);        // Ispis: 123
console.log(userModel.firstName);     // Ispis: Alice
console.log(userModel.accountStatus); // Ispis: active

2. Observables i Povezivanje Podataka (Jezgra Modernih Okvira)

Proxiji su motor iza reaktivnih sustava u modernim okvirima poput Vue 3. Kada promijenite svojstvo na proxied objektu stanja, zamka set se može koristiti za pokretanje ažuriranja u UI ili drugim dijelovima aplikacije.

Ovdje je vrlo pojednostavljen primjer:


function createObservable(target, callback) {
  const handler = {
    set(obj, prop, value) {
      const result = Reflect.set(obj, prop, value);
      callback(prop, value); // Pokrenite povratni poziv prilikom promjene
      return result;
    }
  };
  return new Proxy(target, handler);
}

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

function render(prop, value) {
  console.log(`OTKRIVENA PROMJENA: Svojstvo '${prop}' postavljeno je na '${value}'. Ponovno iscrtavanje UI-ja...`);
}

const observableState = createObservable(state, render);

observableState.count = 1;
// Ispis konzole: OTKRIVENA PROMJENA: Svojstvo 'count' postavljeno je na '1'. Ponovno iscrtavanje UI-ja...

observableState.message = 'Goodbye';
// Ispis konzole: OTKRIVENA PROMJENA: Svojstvo 'message' postavljeno je na 'Goodbye'. Ponovno iscrtavanje UI-ja...

3. Negativni Indeksi Nizova

Klasičan i zabavan primjer je proširenje izvornog ponašanja niza za podršku negativnih indeksa, gdje se -1 odnosi na zadnji element, slično jezicima poput Pythona.


function createNegativeArrayProxy(arr) {
  const handler = {
    get(target, property) {
      const index = Number(property);
      if (!Number.isNaN(index) && index < 0) {
        // Pretvorite negativni indeks u pozitivni s kraja
        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]);  // Ispis: a
console.log(proxiedArray[-1]); // Ispis: e
console.log(proxiedArray[-2]); // Ispis: d
console.log(proxiedArray.length); // Ispis: 5

Razmatranja o Performansama i Najbolje Prakse

Iako su proxiji nevjerojatno moćni, oni nisu čarobni metak. Ključno je razumjeti njihove implikacije.

Režijski Troškovi Performansi

Proxy uvodi sloj indirekcije. Svaka operacija na proxied objektu mora proći kroz rukovatelja, što dodaje malu količinu režijskih troškova u usporedbi s izravnom operacijom na običnom objektu. Za većinu aplikacija (poput validacije podataka ili reaktivnosti na razini okvira), ovi režijski troškovi su zanemarivi. Međutim, u kodu kritičnom za performanse, kao što je čvrsta petlja koja obrađuje milijune stavki, to može postati usko grlo. Uvijek testirajte performanse ako je performanse primarna briga.

Proxy Invarijante

Zamka ne može potpuno lagati o prirodi ciljnog objekta. JavaScript nameće skup pravila zvanih 'invarijante' kojih se zamke proxyja moraju pridržavati. Kršenje invarijante rezultirat će TypeError.

Na primjer, invarijanta za zamku deleteProperty je da ne može vratiti true (što označava uspjeh) ako odgovarajuće svojstvo na ciljnom objektu nije moguće konfigurirati. To sprječava proxy da tvrdi da je izbrisao svojstvo koje se ne može izbrisati.


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

const handler = {
  deleteProperty(target, prop) {
    // Ovo će prekršiti invarijantu
    return true;
  }
};

const proxy = new Proxy(target, handler);

try {
  delete proxy.unbreakable; // Ovo će baciti pogrešku
} catch (e) {
  console.error(e.message);
  // Ispis: 'deleteProperty' na proxyju: vratio true za svojstvo 'unbreakable' koje se ne može konfigurirati
}

Kada Koristiti Proxije (a Kada Ne)

Opozivi Proxiji

Za scenarije u kojima biste možda trebali 'isključiti' proxy (npr. iz sigurnosnih razloga ili upravljanja memorijom), JavaScript pruža Proxy.revocable(). Vraća objekt koji sadrži i proxy i funkciju revoke.


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

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

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

// Sada opozivamo pristup proxyja
revoke();

try {
  console.log(proxy.data); // Ovo će baciti pogrešku
} catch (e) {
  console.error(e.message);
  // Ispis: Ne može se izvršiti 'get' na proxyju koji je opozvan
}

Proxiji vs. Druge Metaprogramske Tehnike

Prije Proxija, programeri su koristili druge metode za postizanje sličnih ciljeva. Korisno je razumjeti kako se Proxiji uspoređuju.

`Object.defineProperty()`

Object.defineProperty() izravno mijenja objekt definiranjem gettera i settera za određena svojstva. Proxiji, s druge strane, uopće ne mijenjaju izvorni objekt; oni ga omotavaju.

Zaključak: Snaga Virtualizacije

JavaScript Proxy API više je od samo pametne značajke; to je temeljna promjena u načinu na koji možemo dizajnirati i komunicirati s objektima. Dopuštajući nam presretanje i prilagodbu temeljnih operacija, Proxiji otvaraju vrata svijetu moćnih uzoraka: od besprijekorne validacije i transformacije podataka do reaktivnih sustava koji pokreću moderna korisnička sučelja.

Iako dolaze s malim troškovima performansi i skupom pravila koje treba slijediti, njihova sposobnost stvaranja čistih, odvojenih i moćnih apstrakcija je neusporediva. Virtualiziranjem objekata možete izgraditi sustave koji su robusniji, lakši za održavanje i izražajniji. Sljedeći put kada se suočite sa složenim izazovom koji uključuje upravljanje podacima, validaciju ili mogućnost promatranja, razmislite je li Proxy pravi alat za posao. Možda je to najelegantnije rješenje u vašem alatu.