Lietuvių

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ą.

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ė.

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.

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ą.

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)

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.

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.