Suomi

Kattava opas globaaleille kehittäjille JavaScript Proxy API:n hallitsemiseen. Opi sieppaamaan ja mukauttamaan objektitoimintoja käytännön esimerkein, käyttötapauksin ja suorituskykyvinkein.

JavaScript Proxy API: Syväsukellus objektien käyttäytymisen muokkaamiseen

Modernin JavaScriptin kehittyvässä maisemassa kehittäjät etsivät jatkuvasti tehokkaampia ja elegantimpia tapoja hallita ja olla vuorovaikutuksessa datan kanssa. Vaikka ominaisuudet kuten luokat, moduulit ja async/await ovat mullistaneet koodin kirjoittamisen, ECMAScript 2015:ssä (ES6) esitelty tehokas metaprogrammointiominaisuus jää usein alikäytetyksi: Proxy API.

Metaprogrammointi saattaa kuulostaa pelottavalta, mutta se on yksinkertaisesti konsepti koodin kirjoittamisesta, joka toimii muun koodin päällä. Proxy API on JavaScriptin ensisijainen työkalu tähän, mahdollistaen "välityspalvelimen" luomisen toiselle objektille, joka voi siepata ja uudelleenmäärittää kyseisen objektin perustavanlaatuiset toiminnot. Se on kuin mukautettavan portinvartijan asettaminen objektin eteen, antaen täydellisen hallinnan sen käyttöön ja muokkaamiseen.

Tämä kattava opas demystifioi Proxy API:n. Tutustumme sen ydinominaisuuksiin, pilkomme sen erilaiset kyvyt käytännön esimerkkien avulla ja käsittelemme edistyneitä käyttötapauksia sekä suorituskykyyn liittyviä näkökohtia. Lopulta ymmärrät, miksi Proxyt ovat modernien viitekehysten kulmakivi ja kuinka voit hyödyntää niitä kirjoittaaksesi puhtaampaa, tehokkaampaa ja ylläpidettävämpää koodia.

Ydinominaisuuksien ymmärtäminen: Kohde, Käsittelijä ja Ansat

Proxy API perustuu kolmeen perustavanlaatuiseen komponenttiin. Niiden roolien ymmärtäminen on avain proxien hallitsemiseen.

Syntaksi välityspalvelimen luomiseen on suoraviivainen:

const proxy = new Proxy(target, handler);

Tarkastellaan hyvin yksinkertaista esimerkkiä. Luomme välityspalvelimen, joka yksinkertaisesti ohjaa kaikki toiminnot kohdeobjektiin käyttämällä tyhjää käsittelijää.


// Alkuperäinen objekti
const target = {
  message: "Hello, World!"
};

// Tyhjä käsittelijä. Kaikki toiminnot ohjataan kohteeseen.
const handler = {};

// Välityspalvelin objekti
const proxy = new Proxy(target, handler);

// Ominaisuuden käyttö välityspalvelimella
console.log(proxy.message); // Tuloste: Hello, World!

// Toiminto ohjattiin kohteeseen
console.log(target.message); // Tuloste: Hello, World!

// Ominaisuuden muokkaaminen välityspalvelimen kautta
proxy.anotherMessage = "Hello, Proxy!";

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

Tässä esimerkissä välityspalvelin toimii täsmälleen kuin alkuperäinen objekti. Todellinen voima tulee, kun alamme määritellä ansoja käsittelijässä.

Proxy-rakenteen anatomia: Yleisten ansojen tutkiminen

Käsittelijäobjekti voi sisältää jopa 13 erilaista ansaa, joista jokainen vastaa JavaScript-objektien perustavanlaatuista sisäistä metodia. Tarkastellaan yleisimpiä ja hyödyllisimpiä.

Ominaisuuksien käyttöön pääsyn ansat

1. get(target, property, receiver)

Tämä on kiistatta eniten käytetty ansa. Se käynnistyy, kun välityspalvelimen ominaisuutta luetaan.

Esimerkki: Oletusarvot olemattomille ominaisuuksille.


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

const userHandler = {
  get(target, property) {
    // Jos ominaisuus on olemassa kohteessa, palauta se.
    // Muuten palauta oletusviesti.
    return property in target ? target[property] : `Ominaisuutta '${property}' ei ole olemassa.`;
  }
};

const userProxy = new Proxy(user, userHandler);

console.log(userProxy.firstName); // Tuloste: John
console.log(userProxy.age);       // Tuloste: 30
console.log(userProxy.country);   // Tuloste: Ominaisuutta 'country' ei ole olemassa.

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

set-ansa kutsutaan, kun välityspalvelimen ominaisuudelle asetetaan arvo. Se on täydellinen validointiin, lokitukseen tai vain luku -objektien luomiseen.

Esimerkki: Tietojen validointi.


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('Iän on oltava kokonaisluku.');
      }
      if (value <= 0) {
        throw new RangeError('Iän on oltava positiivinen luku.');
      }
    }

    // Jos validointi läpäisee, aseta arvo kohdeobjektiin.
    target[property] = value;

    // Ilmoita onnistumisesta.
    return true;
  }
};

const personProxy = new Proxy(person, validationHandler);

personProxy.age = 30; // Tämä on sallittua
console.log(personProxy.age); // Tuloste: 30

try {
  personProxy.age = 'thirty'; // Laukaisee TypeError
} catch (e) {
  console.error(e.message); // Tuloste: Iän on oltava kokonaisluku.
}

try {
  personProxy.age = -5; // Laukaisee RangeError
} catch (e) {
  console.error(e.message); // Tuloste: Iän on oltava positiivinen luku.
}

3. has(target, property)

Tämä ansa sieppaa in-operaattorin. Se mahdollistaa sen hallinnan, mitkä ominaisuudet näkyvät objektissa.

Esimerkki: "Yksityisten" ominaisuuksien piilottaminen.

JavaScriptissa yleinen käytäntö on edeltää yksityisiä ominaisuuksia alaviivalla (_). Voimme käyttää has-ansaa piilottaaksemme ne in-operaattorilta.


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

const hidingHandler = {
  has(target, property) {
    if (property.startsWith('_')) {
      return false; // Teeskennellään, ettei se ole olemassa
    }
    return property in target;
  }
};

const dataProxy = new Proxy(secretData, hidingHandler);

console.log('publicKey' in dataProxy); // Tuloste: true
console.log('_apiKey' in dataProxy);   // Tuloste: false (vaikka se on kohteessa)
console.log('id' in dataProxy);        // Tuloste: true

Huomautus: Tämä vaikuttaa vain in-operaattoriin. Suora käyttö kuten dataProxy._apiKey toimisi edelleen, ellei sinulla ole myös vastaavaa get-ansaa.

4. deleteProperty(target, property)

Tämä ansa suoritetaan, kun ominaisuutta poistetaan delete-operaattorilla. Se on hyödyllinen tärkeiden ominaisuuksien poistamisen estämisessä.

Ansan on palautettava true onnistuneesta poistosta tai false epäonnistuneesta.

Esimerkki: Ominaisuuksien poistamisen estäminen.


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

const deletionGuardHandler = {
  deleteProperty(target, property) {
    if (property in target) {
      console.warn(`Yritetty poistaa suojattua ominaisuutta: '${property}'. Toiminto kielletty.`);
      return false;
    }
    return true; // Ominaisuutta ei ollut joka tapauksessa olemassa
  }
};

const configProxy = new Proxy(immutableConfig, deletionGuardHandler);

delete configProxy.port;
// Konsolin tuloste: Yritetty poistaa suojattua ominaisuutta: 'port'. Toiminto kielletty.

console.log(configProxy.port); // Tuloste: 8080 (Sitä ei poistettu)

Objektin enumerointi ja kuvaus -ansat

5. ownKeys(target)

Tämä ansa käynnistyy operaatioilla, jotka hakevat objektin omien ominaisuuksien luettelon, kuten Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols() ja Reflect.ownKeys().

Esimerkki: Avainten suodattaminen.

Yhdistetään tämä edelliseen "yksityinen" ominaisuus -esimerkkiin piilottaaksemme ne täysin.


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) {
    // Estetään myös suora käyttö
    if (property.startsWith('_')) {
      return undefined;
    }
    return Reflect.get(target, property, receiver);
  }
};

const fullProxy = new Proxy(secretData, keyHidingHandler);

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

Huomaa, että käytämme tässä Reflect:iä. Reflect-objekti tarjoaa metodeja siepattaville JavaScript-operaatioille, ja sen metodien nimet ja allekirjoitukset ovat samat kuin proxy-ansoilla. On parasta käytäntöä käyttää Reflect:iä alkuperäisen operaation välittämiseen kohteelle, varmistaen että oletuskäyttäytyminen säilyy oikein.

Funktio- ja konstruktori-ansat

Välityspalvelimet eivät rajoitu pelkkiin objekteihin. Kun kohde on funktio, voit siepata kutsuja ja konstruktioita.

6. apply(target, thisArg, argumentsList)

Tämä ansa kutsutaan, kun funktion välityspalvelinta suoritetaan. Se sieppaa funktion kutsun.

Esimerkki: Funktiokutsujen ja niiden argumenttien lokitus.


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

const loggingHandler = {
  apply(target, thisArg, argumentsList) {
    console.log(`Kutsutaan funktiota '${target.name}' argumenteilla: ${argumentsList}`);
    // Suoritetaan alkuperäinen funktio oikealla kontekstilla ja argumenteilla
    const result = Reflect.apply(target, thisArg, argumentsList);
    console.log(`Funktio '${target.name}' palautti: ${result}`);
    return result;
  }
};

const proxiedSum = new Proxy(sum, loggingHandler);

proxiedSum(5, 10);
// Konsolin tuloste:
// Kutsutaan funktiota 'sum' argumenteilla: 5,10
// Funktio 'sum' palautti: 15

7. construct(target, argumentsList, newTarget)

Tämä ansa sieppaa new-operaattorin käytön luokan tai funktion välityspalvelimella.

Esimerkki: Singleton-mallin toteutus.


class MyDatabaseConnection {
  constructor(url) {
    this.url = url;
    console.log(`Yhdistetään kohteeseen ${this.url}...`);
  }
}

let instance;

const singletonHandler = {
  construct(target, argumentsList) {
    if (!instance) {
      console.log('Luodaan uutta instanssia.');
      instance = Reflect.construct(target, argumentsList);
    }
    console.log('Palautetaan olemassa oleva instanssi.');
    return instance;
  }
};

const ProxiedConnection = new Proxy(MyDatabaseConnection, singletonHandler);

const conn1 = new ProxiedConnection('db://primary');
// Konsolin tuloste:
// Luodaan uutta instanssia.
// Yhdistetään kohteeseen db://primary...
// Palautetaan olemassa oleva instanssi.

const conn2 = new ProxiedConnection('db://secondary'); // URL ohitetaan
// Konsolin tuloste:
// Palautetaan olemassa oleva instanssi.

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

Käytännön käyttötapaukset ja edistyneet mallit

Nyt kun olemme käsitelleet yksittäiset ansat, katsotaan kuinka niitä voidaan yhdistää todellisten ongelmien ratkaisemiseksi.

1. API-abstraktio ja datamuunnos

API:t palauttavat usein dataa muodossa, joka ei vastaa sovelluksesi käytäntöjä (esim. snake_case vs. camelCase). Välityspalvelin voi hoitaa tämän muunnoksen läpinäkyvästi.


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

// Kuvittele, että tämä on raakadata API:sta
const apiResponse = {
  user_id: 123,
  first_name: 'Alice',
  last_name: 'Wonderland',
  account_status: 'active'
};

const camelCaseHandler = {
  get(target, property) {
    const camelCaseProperty = snakeToCamel(property);
    // Tarkistetaan, onko camelCase-versio suoraan olemassa
    if (camelCaseProperty in target) {
      return target[camelCaseProperty];
    }
    // Takaisinpäin alkuperäiseen ominaisuusnimeen
    if (property in target) {
      return target[property];
    }
    return undefined;
  }
};

const userModel = new Proxy(apiResponse, camelCaseHandler);

// Voimme nyt käyttää ominaisuuksia camelCase-muodossa, vaikka ne on tallennettu snake_case-muodossa
console.log(userModel.userId);        // Tuloste: 123
console.log(userModel.firstName);     // Tuloste: Alice
console.log(userModel.accountStatus); // Tuloste: active

2. Tarkkailtavat ja datan sitominen (Modernien viitekehysten ydin)

Välityspalvelimet ovat käyttöliittymän reaktiivisuusjärjestelmien moottori moderneissa viitekehyksissä kuten Vue 3. Kun muutat proxyaatun tiliobjektin ominaisuutta, set-ansaa voidaan käyttää päivitysten käynnistämiseen käyttöliittymässä tai muissa sovelluksen osissa.

Tässä hyvin yksinkertaistettu esimerkki:


function createObservable(target, callback) {
  const handler = {
    set(obj, prop, value) {
      const result = Reflect.set(obj, prop, value);
      callback(prop, value); // Käynnistää takaisinkutsun muutoksesta
      return result;
    }
  };
  return new Proxy(target, handler);
}

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

function render(prop, value) {
  console.log(`MUUTOS TULKITTU: Ominaisuus '${prop}' asetettiin arvoon '${value}'. Käyttöliittymä renderöidään uudelleen...`);
}

const observableState = createObservable(state, render);

observableState.count = 1;
// Konsolin tuloste: MUUTOS TULKITTU: Ominaisuus 'count' asetettiin arvoon '1'. Käyttöliittymä renderöidään uudelleen...

observableState.message = 'Goodbye';
// Konsolin tuloste: MUUTOS TULKITTU: Ominaisuus 'message' asetettiin arvoon 'Goodbye'. Käyttöliittymä renderöidään uudelleen...

3. Negatiiviset taulukkoindeksit

Klassinen ja hauska esimerkki on natiivin taulukon toiminnallisuuden laajentaminen tukemaan negatiivisia indeksejä, jossa -1 viittaa viimeiseen elementtiin, samankaltaisesti kuin Pythonin kaltaisissa kielissä.


function createNegativeArrayProxy(arr) {
  const handler = {
    get(target, property) {
      const index = Number(property);
      if (!Number.isNaN(index) && index < 0) {
        // Muunnetaan negatiivinen indeksi positiiviseksi lopusta päin
        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]);  // Tuloste: a
console.log(proxiedArray[-1]); // Tuloste: e
console.log(proxiedArray[-2]); // Tuloste: d
console.log(proxiedArray.length); // Tuloste: 5

Suorituskykyyn liittyvät huomiot ja parhaat käytännöt

Vaikka välityspalvelimet ovat uskomattoman tehokkaita, ne eivät ole taikatemppu. On tärkeää ymmärtää niiden seuraukset.

Suorituskykyyn liittyvä lisärasitus

Välityspalvelin lisää välillisen kerroksen. Jokaisen proxyaatulle objektille tehdyn operaation on kuljettava käsittelijän läpi, mikä lisää pienen määrän lisärasitusta verrattuna suoraan operaatioon tavallisella objektilla. Useimmissa sovelluksissa (kuten datan validointi tai viitekehystason reaktiivisuus) tämä lisärasitus on merkityksetön. Suorituskykykriittisessä koodissa, kuten tiukassa silmukassa, joka käsittelee miljoonia kohteita, tämä voi kuitenkin muodostua pullonkaulaksi. Mittaa aina, jos suorituskyky on ensisijainen huolenaihe.

Välityspalvelimen invariantit

Ansa ei voi täysin valehdella kohdeobjektin luonteesta. JavaScript valvoo joukkoa sääntöjä, joita kutsutaan "invariantteiksi", joita välityspalvelimen ansojen on noudatettava. Invariantin rikkominen aiheuttaa TypeError-virheen.

Esimerkiksi deleteProperty-ansan invariantti on, että se ei voi palauttaa true (merkiten onnistumista), jos vastaava ominaisuus kohdeobjektissa on ei-konfiguroitavissa. Tämä estää välityspalvelintä väittämästä, että se poisti ominaisuuden, jota ei voi poistaa.


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

const handler = {
  deleteProperty(target, prop) {
    // Tämä rikkoo invariantin
    return true;
  }
};

const proxy = new Proxy(target, handler);

try {
  delete proxy.unbreakable; // Tämä aiheuttaa virheen
} catch (e) {
  console.error(e.message);
  // Tuloste: 'deleteProperty' proxyllä: palautti true ei-konfiguroitavalle ominaisuudelle 'unbreakable'
}

Milloin käyttää välityspalvelimia (ja milloin ei)

Peruutettavat välityspalvelimet

Skenaarioissa, joissa saatat joutua "sammuttamaan" välityspalvelimen (esim. turvallisuussyistä tai muistinhallinnan vuoksi), JavaScript tarjoaa Proxy.revocable(). Se palauttaa objektin, joka sisältää sekä välityspalvelimen että revoke-funktion.


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

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

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

// Nyt peruutamme välityspalvelimen käyttöoikeuden
revoke();

try {
  console.log(proxy.data); // Tämä aiheuttaa virheen
} catch (e) {
  console.error(e.message);
  // Tuloste: Ei voida suorittaa 'get' proxyllä, joka on peruutettu
}

Välityspalvelimet vs. muut metaprogrammointitekniikat

Ennen välityspalvelimia kehittäjät käyttivät muita menetelmiä vastaavien tavoitteiden saavuttamiseksi. On hyödyllistä ymmärtää, miten välityspalvelimet vertautuvat.

Object.defineProperty()

Object.defineProperty() muokkaa objektia suoraan määrittelemällä getterit ja setterit tietyille ominaisuuksille. Välityspalvelimet sen sijaan eivät muokkaa alkuperäistä objektia lainkaan; ne käärivät sen.

Yhteenveto: Virtualisoinnin voima

JavaScript Proxy API on enemmän kuin pelkkä fiksu ominaisuus; se on perustavanlaatuinen muutos siinä, kuinka voimme suunnitella ja olla vuorovaikutuksessa objektien kanssa. Mahdollistamalla perustavanlaatuisten operaatioiden sieppaamisen ja mukauttamisen, välityspalvelimet avaavat oven tehokkaiden mallien maailmaan: saumattomasta datan validoinnista ja muunnoksesta aina reaktiivisiin järjestelmiin, jotka pyörittävät moderneja käyttöliittymiä.

Vaikka niihin liittyy pieni suorituskykykustannus ja joukko sääntöjä, niiden kyky luoda puhtaita, irrotettuja ja tehokkaita abstraktioita on vertaansa vailla. Virtualisoimalla objekteja voit rakentaa järjestelmiä, jotka ovat kestävämpiä, ylläpidettävämpiä ja ilmeikkäämpiä. Seuraavan kerran kun kohtaat monimutkaisen haasteen liittyen datanhallintaan, validointiin tai havainnointiin, harkitse, onko välityspalvelin oikea työkalu tehtävään. Se voi olla juuri kaikkein elegantin ratkaisun työkalupakissasi.