Eesti

Põhjalik juhend arendajatele JavaScripti Proxy API valdamiseks. Õppige pealt kuulama ja kohandama objektide operatsioone praktiliste näidete, kasutusjuhtude ja jõudlusnippidega.

JavaScripti Proxy API: Süvitsiminek objekti käitumise muutmisel

Kaasaegse JavaScripti areneval maastikul otsivad arendajad pidevalt võimsamaid ja elegantsemaid viise andmete haldamiseks ja nendega suhtlemiseks. Kuigi funktsioonid nagu klassid, moodulid ja async/await on muutnud meie koodi kirjutamise viisi revolutsiooniliseks, on ECMAScript 2015 (ES6) versioonis tutvustatud võimas metaprogrammeerimise funktsioon, mis sageli jääb alakasutatuks: Proxy API.

Metaprogrammeerimine võib tunduda hirmutav, kuid see on lihtsalt kontseptsioon koodi kirjutamisest, mis opereerib teise koodiga. Proxy API on JavaScripti peamine tööriist selleks, võimaldades luua teise objekti jaoks 'proksi', mis suudab pealt kuulata ja uuesti defineerida selle objekti põhilisi operatsioone. See on nagu kohandatava väravavahi paigutamine objekti ette, andes teile täieliku kontrolli selle üle, kuidas sellele juurde pääsetakse ja seda muudetakse.

See põhjalik juhend aitab Proxy API müstikat hajutada. Uurime selle põhimõisteid, analüüsime selle erinevaid võimekusi praktiliste näidete abil ning arutame edasijõudnud kasutusjuhte ja jõudlusega seotud kaalutlusi. Lõpuks saate aru, miks proxyd on kaasaegsete raamistike nurgakivi ja kuidas saate neid kasutada puhtama, võimsama ja paremini hooldatava koodi kirjutamiseks.

Põhimõistete mõistmine: sihtmärk, käsitleja ja püünised

Proxy API on üles ehitatud kolmele põhikomponendile. Nende rollide mõistmine on proxy'de valdamise võti.

Proxy loomise süntaks on otsekohene:

const proxy = new Proxy(target, handler);

Vaatame ühte väga lihtsat näidet. Loome proxy, mis lihtsalt edastab kõik operatsioonid sihtmärk-objektile, kasutades tühja käsitlejat.


// Algne objekt
const target = {
  message: "Tere, Maailm!"
};

// Tühi käsitleja. Kõik operatsioonid edastatakse sihtmärgile.
const handler = {};

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

// Omadusele juurdepääs proxy kaudu
console.log(proxy.message); // Väljund: Tere, Maailm!

// Operatsioon edastati sihtmärgile
console.log(target.message); // Väljund: Tere, Maailm!

// Omaduse muutmine proxy kaudu
proxy.anotherMessage = "Tere, Proxy!";

console.log(proxy.anotherMessage); // Väljund: Tere, Proxy!
console.log(target.anotherMessage); // Väljund: Tere, Proxy!

Selles näites käitub proxy täpselt nagu algne objekt. Tõeline võimsus ilmneb siis, kui hakkame käsitlejas püüniseid defineerima.

Proxy anatoomia: levinumate püüniste uurimine

Käsitleja objekt võib sisaldada kuni 13 erinevat püünist, millest igaüks vastab JavaScripti objektide fundamentaalsele sisemisele meetodile. Uurime kõige levinumaid ja kasulikumaid neist.

Omadustele juurdepääsu püünised

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

See on vaieldamatult kõige kasutatavam püünis. See käivitub, kui proxy omadust loetakse.

Näide: Vaikimisi väärtused olematutele omadustele.


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

const userHandler = {
  get(target, property) {
    // Kui omadus on sihtmärgis olemas, tagasta see.
    // Vastasel juhul tagasta vaike-sõnum.
    return property in target ? target[property] : `Omadust '${property}' ei eksisteeri.`;
  }
};

const userProxy = new Proxy(user, userHandler);

console.log(userProxy.firstName); // Väljund: John
console.log(userProxy.age);       // Väljund: 30
console.log(userProxy.country);   // Väljund: Omadust 'country' ei eksisteeri.

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

Püünis set kutsutakse välja, kui proxy omadusele omistatakse väärtus. See on ideaalne valideerimiseks, logimiseks või ainult lugemiseks mõeldud objektide loomiseks.

Näide: Andmete valideerimine.


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('Vanus peab olema täisarv.');
      }
      if (value <= 0) {
        throw new RangeError('Vanus peab olema positiivne arv.');
      }
    }

    // Kui valideerimine õnnestub, määra väärtus sihtmärk-objektile.
    target[property] = value;

    // Märgi õnnestumist.
    return true;
  }
};

const personProxy = new Proxy(person, validationHandler);

personProxy.age = 30; // See on kehtiv
console.log(personProxy.age); // Väljund: 30

try {
  personProxy.age = 'kolmkümmend'; // Visatakse TypeError
} catch (e) {
  console.error(e.message); // Väljund: Vanus peab olema täisarv.
}

try {
  personProxy.age = -5; // Visatakse RangeError
} catch (e) {
  console.error(e.message); // Väljund: Vanus peab olema positiivne arv.
}

3. `has(target, property)`

See püünis kuulab pealt in operaatorit. See võimaldab teil kontrollida, millised omadused tunduvad objektil eksisteerivat.

Näide: 'Privaatsete' omaduste peitmine.

JavaScriptis on levinud tava lisada privaatsete omaduste ette allkriips (_). Saame kasutada has püünist, et peita need in operaatori eest.


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

const hidingHandler = {
  has(target, property) {
    if (property.startsWith('_')) {
      return false; // Teeskle, et seda pole olemas
    }
    return property in target;
  }
};

const dataProxy = new Proxy(secretData, hidingHandler);

console.log('publicKey' in dataProxy); // Väljund: true
console.log('_apiKey' in dataProxy);   // Väljund: false (kuigi see on sihtmärgil olemas)
console.log('id' in dataProxy);        // Väljund: true

Märkus: See mõjutab ainult in operaatorit. Otsene juurdepääs nagu dataProxy._apiKey toimiks endiselt, kui te ei implementeeri ka vastavat get püünist.

4. `deleteProperty(target, property)`

See püünis käivitatakse, kui omadus kustutatakse operaatoriga delete. See on kasulik oluliste omaduste kustutamise vältimiseks.

Püünis peab tagastama true õnnestunud kustutamise korral või false ebaõnnestunud kustutamise korral.

Näide: Omaduste kustutamise vältimine.


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

const deletionGuardHandler = {
  deleteProperty(target, property) {
    if (property in target) {
      console.warn(`Katse kustutada kaitstud omadust: '${property}'. Operatsioon keelatud.`);
      return false;
    }
    return true; // Omadust niikuinii ei eksisteerinud
  }
};

const configProxy = new Proxy(immutableConfig, deletionGuardHandler);

delete configProxy.port;
// Konsooli väljund: Katse kustutada kaitstud omadust: 'port'. Operatsioon keelatud.

console.log(configProxy.port); // Väljund: 8080 (Seda ei kustutatud)

Objekti loetlemise ja kirjeldamise püünised

5. `ownKeys(target)`

See püünis käivitatakse operatsioonide puhul, mis hangivad objekti enda omaduste loendi, nagu Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols() ja Reflect.ownKeys().

Näide: Võtmete filtreerimine.

Kombineerime selle meie eelmise 'privaatse' omaduse näitega, et need täielikult peita.


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) {
    // Takista ka otsest juurdepääsu
    if (property.startsWith('_')) {
      return undefined;
    }
    return Reflect.get(target, property, receiver);
  }
};

const fullProxy = new Proxy(secretData, keyHidingHandler);

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

Pange tähele, et me kasutame siin Reflect objekti. Reflect objekt pakub meetodeid pealtkuulatavate JavaScripti operatsioonide jaoks ning selle meetoditel on samad nimed ja signatuurid kui proxy püünistel. Parim praktika on kasutada Reflect objekti algse operatsiooni edastamiseks sihtmärgile, tagades vaikekäitumise korrektse säilimise.

Funktsiooni ja konstruktori püünised

Proxy'd ei piirdu ainult tavaliste objektidega. Kui sihtmärk on funktsioon, saate pealt kuulata kutseid ja konstruktsioone.

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

See püünis kutsutakse välja, kui funktsiooni proxy't käivitatakse. See kuulab pealt funktsioonikutset.

Näide: Funktsioonikutsete ja nende argumentide logimine.


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

const loggingHandler = {
  apply(target, thisArg, argumentsList) {
    console.log(`Kutsutakse funktsiooni '${target.name}' argumentidega: ${argumentsList}`);
    // Käivita algne funktsioon õige konteksti ja argumentidega
    const result = Reflect.apply(target, thisArg, argumentsList);
    console.log(`Funktsioon '${target.name}' tagastas: ${result}`);
    return result;
  }
};

const proxiedSum = new Proxy(sum, loggingHandler);

proxiedSum(5, 10);
// Konsooli väljund:
// Kutsutakse funktsiooni 'sum' argumentidega: 5,10
// Funktsioon 'sum' tagastas: 15

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

See püünis kuulab pealt new operaatori kasutamist klassi või funktsiooni proxy'l.

Näide: Singleton mustri implementeerimine.


class MyDatabaseConnection {
  constructor(url) {
    this.url = url;
    console.log(`Ühendun aadressiga ${this.url}...`);
  }
}

let instance;

const singletonHandler = {
  construct(target, argumentsList) {
    if (!instance) {
      console.log('Luuakse uus eksemplar.');
      instance = Reflect.construct(target, argumentsList);
    }
    console.log('Tagastatakse olemasolev eksemplar.');
    return instance;
  }
};

const ProxiedConnection = new Proxy(MyDatabaseConnection, singletonHandler);

const conn1 = new ProxiedConnection('db://primary');
// Konsooli väljund:
// Luuakse uus eksemplar.
// Ühendun aadressiga db://primary...
// Tagastatakse olemasolev eksemplar.

const conn2 = new ProxiedConnection('db://secondary'); // URL-i ignoreeritakse
// Konsooli väljund:
// Tagastatakse olemasolev eksemplar.

console.log(conn1 === conn2); // Väljund: true
console.log(conn1.url); // Väljund: db://primary
console.log(conn2.url); // Väljund: db://primary

Praktilised kasutusjuhud ja edasijõudnud mustrid

Nüüd, kui oleme üksikud püünised läbi käinud, vaatame, kuidas neid saab kombineerida reaalsete probleemide lahendamiseks.

1. API abstraktsioon ja andmete teisendamine

API-d tagastavad sageli andmeid formaadis, mis ei vasta teie rakenduse tavadele (nt snake_case vs. camelCase). Proxy saab selle teisenduse läbipaistvalt käsitleda.


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

// Kujutage ette, et see on meie toorandmestik API-st
const apiResponse = {
  user_id: 123,
  first_name: 'Alice',
  last_name: 'Wonderland',
  account_status: 'active'
};

const camelCaseHandler = {
  get(target, property) {
    const camelCaseProperty = snakeToCamel(property);
    // Kontrolli, kas camelCase versioon on otse olemas
    if (camelCaseProperty in target) {
      return target[camelCaseProperty];
    }
    // Varuvariant on algne omaduse nimi
    if (property in target) {
      return target[property];
    }
    return undefined;
  }
};

const userModel = new Proxy(apiResponse, camelCaseHandler);

// Nüüd saame omadustele ligi camelCase'i abil, kuigi need on salvestatud snake_case'ina
console.log(userModel.userId);        // Väljund: 123
console.log(userModel.firstName);     // Väljund: Alice
console.log(userModel.accountStatus); // Väljund: active

2. Jälgitavad (Observables) ja andmesidumine (Data Binding) (kaasaegsete raamistike tuum)

Proxy'd on mootoriks reaktiivsussüsteemide taga kaasaegsetes raamistikes nagu Vue 3. Kui muudate proksitud olekuobjekti omadust, saab set püünist kasutada uuenduste käivitamiseks kasutajaliideses või rakenduse muudes osades.

Siin on väga lihtsustatud näide:


function createObservable(target, callback) {
  const handler = {
    set(obj, prop, value) {
      const result = Reflect.set(obj, prop, value);
      callback(prop, value); // Käivita tagasikutse muudatuse korral
      return result;
    }
  };
  return new Proxy(target, handler);
}

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

function render(prop, value) {
  console.log(`MUUDATUS TUVASTATUD: Omadus '${prop}' seati väärtusele '${value}'. Kasutajaliidese uuesti renderdamine...`);
}

const observableState = createObservable(state, render);

observableState.count = 1;
// Konsooli väljund: MUUDATUS TUVASTATUD: Omadus 'count' seati väärtusele '1'. Kasutajaliidese uuesti renderdamine...

observableState.message = 'Hüvasti';
// Konsooli väljund: MUUDATUS TUVASTATUD: Omadus 'message' seati väärtusele 'Hüvasti'. Kasutajaliidese uuesti renderdamine...

3. Negatiivsed massiiviindeksid

Klassikaline ja lõbus näide on natiivse massiivi käitumise laiendamine negatiivsete indeksite toetamiseks, kus -1 viitab viimasele elemendile, sarnaselt keeltega nagu Python.


function createNegativeArrayProxy(arr) {
  const handler = {
    get(target, property) {
      const index = Number(property);
      if (!Number.isNaN(index) && index < 0) {
        // Teisenda negatiivne indeks positiivseks, lugedes lõpust
        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äljund: a
console.log(proxiedArray[-1]); // Väljund: e
console.log(proxiedArray[-2]); // Väljund: d
console.log(proxiedArray.length); // Väljund: 5

Jõudlusega seotud kaalutlused ja parimad praktikad

Kuigi proxy'd on uskumatult võimsad, ei ole nad imerohi. On oluline mõista nende mõju.

Jõudluse lisakulu

Proxy lisab kaudse kihi. Iga proksitud objektiga tehtav operatsioon peab läbima käsitleja, mis lisab väikese hulga lisakulu võrreldes otseoperatsiooniga tavalisel objektil. Enamiku rakenduste jaoks (nagu andmete valideerimine või raamistiku tasemel reaktiivsus) on see lisakulu tühine. Kuid jõudluskriitilises koodis, näiteks tihedas tsüklis, mis töötleb miljoneid elemente, võib see muutuda pudelikaelaks. Tehke alati võrdlusteste, kui jõudlus on esmatähtis.

Proxy invariandid

Püünis ei saa sihtmärk-objekti olemuse kohta täielikult valetada. JavaScript jõustab reeglite kogumit, mida nimetatakse 'invariantideks', mida proxy püünised peavad järgima. Invariandi rikkumine põhjustab TypeError vea.

Näiteks deleteProperty püüdmise invariant on see, et see ei saa tagastada väärtust true (mis näitab õnnestumist), kui vastav omadus sihtmärk-objektil ei ole konfigureeritav. See takistab proxy'l väitmast, et ta kustutas omaduse, mida ei saa kustutada.


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

const handler = {
  deleteProperty(target, prop) {
    // See rikub invarianti
    return true;
  }
};

const proxy = new Proxy(target, handler);

try {
  delete proxy.unbreakable; // See viskab vea
} catch (e) {
  console.error(e.message);
  // Väljund: 'deleteProperty' on proxy: returned true for non-configurable property 'unbreakable'
}

Millal kasutada proxy'sid (ja millal mitte)

Tühistatavad proxy'd

Olukordadeks, kus teil võib tekkida vajadus proxy 'välja lülitada' (nt turvakaalutlustel või mäluhalduseks), pakub JavaScript Proxy.revocable(). See tagastab objekti, mis sisaldab nii proxy't kui ka revoke funktsiooni.


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

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

console.log(proxy.data); // Väljund: tundlik

// Nüüd tühistame proxy juurdepääsu
revoke();

try {
  console.log(proxy.data); // See viskab vea
} catch (e) {
  console.error(e.message);
  // Väljund: Cannot perform 'get' on a proxy that has been revoked
}

Proxy'd vs. teised metaprogrammeerimise tehnikad

Enne proxy'de tulekut kasutasid arendajad sarnaste eesmärkide saavutamiseks teisi meetodeid. On kasulik mõista, kuidas proxy'd nendega võrreldes toimivad.

`Object.defineProperty()`

Object.defineProperty() muudab objekti otse, defineerides getterid ja setterid konkreetsete omaduste jaoks. Proxy'd seevastu ei muuda algset objekti üldse; nad mähivad selle.

Kokkuvõte: Virtualiseerimise jõud

JavaScripti Proxy API on enamat kui lihtsalt nutikas funktsioon; see on fundamentaalne nihe selles, kuidas me saame objekte disainida ja nendega suhelda. Võimaldades meil pealt kuulata ja kohandada põhilisi operatsioone, avavad proxy'd ukse võimsate mustrite maailma: alates sujuvast andmete valideerimisest ja teisendamisest kuni reaktiivsete süsteemideni, mis on kaasaegsete kasutajaliideste jõuallikaks.

Kuigi nendega kaasneb väike jõudluskulu ja hulk reegleid, mida tuleb järgida, on nende võime luua puhtaid, lahtisidestatud ja võimsaid abstraktsioone võrratu. Objekte virtualiseerides saate ehitada süsteeme, mis on vastupidavamad, hooldatavamad ja väljendusrikkamad. Järgmine kord, kui seisate silmitsi keerulise väljakutsega, mis hõlmab andmehaldust, valideerimist või jälgitavust, kaaluge, kas proxy on selle töö jaoks õige tööriist. See võib olla just kõige elegantsem lahendus teie tööriistakastis.