Latviešu

Visaptverošs ceļvedis globāliem izstrādātājiem JavaScript Proxy API apgūšanai. Iemācieties pārtvert un pielāgot objektu darbības ar praktiskiem piemēriem, lietošanas gadījumiem un veiktspējas padomiem.

JavaScript Proxy API: Dziļš Ieskats Objektu Uzvedības Modifikācijā

Mūsdienu JavaScript mainīgajā ainavā izstrādātāji pastāvīgi meklē spēcīgākus un elegantākus veidus, kā pārvaldīt un mijiedarboties ar datiem. Lai gan tādas funkcijas kā klases, moduļi un async/await ir revolucionizējušas veidu, kā mēs rakstām kodu, ECMAScript 2015 (ES6) tika ieviesta spēcīga metaprogrammēšanas funkcija, kas bieži paliek nepietiekami izmantota: Proxy API.

Metaprogrammēšana var izklausīties biedējoši, bet tas ir vienkārši jēdziens par koda rakstīšanu, kas darbojas ar citu kodu. Proxy API ir JavaScript galvenais rīks šim nolūkam, kas ļauj izveidot "proxy" citam objektam, kas var pārtvert un no jauna definēt šī objekta fundamentālās darbības. Tas ir kā pielāgojama vārtsarga novietošana objekta priekšā, sniedzot jums pilnīgu kontroli pār to, kā tam piekļūst un kā tas tiek modificēts.

Šis visaptverošais ceļvedis demistificēs Proxy API. Mēs izpētīsim tā pamatjēdzienus, sadalīsim tā dažādās iespējas ar praktiskiem piemēriem un apspriedīsim uzlabotus lietošanas gadījumus un veiktspējas apsvērumus. Beigās jūs sapratīsiet, kāpēc Proxies ir mūsdienu ietvaru stūrakmens un kā jūs varat tos izmantot, lai rakstītu tīrāku, spēcīgāku un vieglāk uzturamu kodu.

Pamatjēdzienu izpratne: Mērķis, Apstrādātājs un Slazdi

Proxy API ir balstīts uz trim fundamentāliem komponentiem. Izpratne par to lomām ir atslēga proxy apgūšanai.

Proxy izveides sintakse ir vienkārša:

const proxy = new Proxy(target, handler);

Apskatīsim ļoti vienkāršu piemēru. Mēs izveidosim proxy, kas vienkārši nodod visas darbības mērķa objektam, izmantojot tukšu apstrādātāju.


// The original object
const target = {
  message: "Hello, World!"
};

// An empty handler. All operations will be forwarded to the target.
const handler = {};

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

// Accessing a property on the proxy
console.log(proxy.message); // Output: Hello, World!

// The operation was forwarded to the target
console.log(target.message); // Output: Hello, World!

// Modifying a property through the proxy
proxy.anotherMessage = "Hello, Proxy!";

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

Šajā piemērā proxy uzvedas tieši tāpat kā oriģinālais objekts. Īstais spēks nāk tad, kad mēs sākam definēt slazdus apstrādātājā.

Proxy anatomija: Izpētot biežāk sastopamos slazdus

Apstrādātāja objekts var saturēt līdz 13 dažādiem slazdiem, katrs no tiem atbilst JavaScript objektu fundamentālai iekšējai metodei. Izpētīsim visbiežāk sastopamos un noderīgos.

Rekvizītu piekļuves slazdi

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

Šis, iespējams, ir visvairāk izmantotais slazds. Tas tiek aktivizēts, kad tiek nolasīts proxy rekvizīts.

Piemērs: Noklusējuma vērtības neeksistējošiem rekvizītiem.


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

const userHandler = {
  get(target, property) {
    // If the property exists on the target, return it.
    // Otherwise, return a default message.
    return property in target ? target[property] : `Property '${property}' does not exist.`;
  }
};

const userProxy = new Proxy(user, userHandler);

console.log(userProxy.firstName); // Output: John
console.log(userProxy.age);       // Output: 30
console.log(userProxy.country);   // Output: Property 'country' does not exist.

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

set slazds tiek izsaukts, kad proxy rekvizītam tiek piešķirta vērtība. Tas ir lieliski piemērots validācijai, reģistrēšanai vai tikai lasāmu objektu izveidei.

Piemērs: Datu validācija.


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('Age must be an integer.');
      }
      if (value <= 0) {
        throw new RangeError('Age must be a positive number.');
      }
    }

    // If validation passes, set the value on the target object.
    target[property] = value;

    // Indicate success.
    return true;
  }
};

const personProxy = new Proxy(person, validationHandler);

personProxy.age = 30; // This is valid
console.log(personProxy.age); // Output: 30

try {
  personProxy.age = 'thirty'; // Throws TypeError
} catch (e) {
  console.error(e.message); // Output: Age must be an integer.
}

try {
  personProxy.age = -5; // Throws RangeError
} catch (e) {
  console.error(e.message); // Output: Age must be a positive number.
}

3. `has(target, property)`

Šis slazds pārtver in operatoru. Tas ļauj jums kontrolēt, kuri rekvizīti šķiet pastāvam objektā.

Piemērs: "Privātu" rekvizītu slēpšana.

JavaScript izplatīta konvencija ir prefiksēt privātos rekvizītus ar pasvītrojumu (_). Mēs varam izmantot has slazdu, lai tos paslēptu no in operatora.


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

const hidingHandler = {
  has(target, property) {
    if (property.startsWith('_')) {
      return false; // Pretend it doesn't exist
    }
    return property in target;
  }
};

const dataProxy = new Proxy(secretData, hidingHandler);

console.log('publicKey' in dataProxy); // Output: true
console.log('_apiKey' in dataProxy);   // Output: false (even though it's on the target)
console.log('id' in dataProxy);        // Output: true

Piezīme: Tas ietekmē tikai in operatoru. Tieša piekļuve, piemēram, dataProxy._apiKey, joprojām darbotos, ja vien jūs neieviešat arī atbilstošu get slazdu.

4. `deleteProperty(target, property)`

Šis slazds tiek izpildīts, kad rekvizīts tiek dzēsts, izmantojot delete operatoru. Tas ir noderīgi, lai novērstu svarīgu rekvizītu dzēšanu.

Slazdam jāatgriež true, lai dzēšana būtu veiksmīga, vai false, ja dzēšana neizdevās.

Piemērs: Rekvizītu dzēšanas novēršana.


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

const deletionGuardHandler = {
  deleteProperty(target, property) {
    if (property in target) {
      console.warn(`Attempted to delete protected property: '${property}'. Operation denied.`);
      return false;
    }
    return true; // Property didn't exist anyway
  }
};

const configProxy = new Proxy(immutableConfig, deletionGuardHandler);

delete configProxy.port;
// Console output: Attempted to delete protected property: 'port'. Operation denied.

console.log(configProxy.port); // Output: 8080 (It wasn't deleted)

Objektu uzskaitīšanas un apraksta slazdi

5. `ownKeys(target)`

Šo slazdu aktivizē darbības, kas iegūst objekta pašu rekvizītu sarakstu, piemēram, Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols() un Reflect.ownKeys().

Piemērs: Atslēgu filtrēšana.

Apvienosim to ar mūsu iepriekšējo "privāto" rekvizītu piemēru, lai tos pilnībā paslēptu.


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) {
    // Also prevent direct access
    if (property.startsWith('_')) {
      return undefined;
    }
    return Reflect.get(target, property, receiver);
  }
};

const fullProxy = new Proxy(secretData, keyHidingHandler);

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

Ievērojiet, ka mēs šeit izmantojam Reflect. Reflect objekts nodrošina metodes pārtveramām JavaScript darbībām, un tā metodēm ir tādi paši nosaukumi un paraksti kā proxy slazdiem. Ir laba prakse izmantot Reflect, lai pārsūtītu oriģinālo darbību uz mērķi, nodrošinot, ka tiek pareizi uzturēta noklusējuma darbība.

Funkciju un konstruktoru slazdi

Proxy neaprobežojas tikai ar vienkāršiem objektiem. Kad mērķis ir funkcija, jūs varat pārtvert izsaukumus un konstrukcijas.

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

Šis slazds tiek izsaukts, kad tiek izpildīts funkcijas proxy. Tas pārtver funkcijas izsaukumu.

Piemērs: Funkciju izsaukumu un to argumentu reģistrēšana.


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

const loggingHandler = {
  apply(target, thisArg, argumentsList) {
    console.log(`Calling function '${target.name}' with arguments: ${argumentsList}`);
    // Execute the original function with the correct context and arguments
    const result = Reflect.apply(target, thisArg, argumentsList);
    console.log(`Function '${target.name}' returned: ${result}`);
    return result;
  }
};

const proxiedSum = new Proxy(sum, loggingHandler);

proxiedSum(5, 10);
// Console output:
// Calling function 'sum' with arguments: 5,10
// Function 'sum' returned: 15

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

Šis slazds pārtver new operatora izmantošanu klases vai funkcijas proxy.

Piemērs: Singleton modeļa ieviešana.


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

let instance;

const singletonHandler = {
  construct(target, argumentsList) {
    if (!instance) {
      console.log('Creating new instance.');
      instance = Reflect.construct(target, argumentsList);
    }
    console.log('Returning existing instance.');
    return instance;
  }
};

const ProxiedConnection = new Proxy(MyDatabaseConnection, singletonHandler);

const conn1 = new ProxiedConnection('db://primary');
// Console output:
// Creating new instance.
// Connecting to db://primary...
// Returning existing instance.

const conn2 = new ProxiedConnection('db://secondary'); // URL will be ignored
// Console output:
// Returning existing instance.

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

Praktiski lietošanas gadījumi un uzlaboti modeļi

Tagad, kad esam apskatījuši atsevišķus slazdus, apskatīsim, kā tos var apvienot, lai atrisinātu reālās pasaules problēmas.

1. API abstrakcija un datu transformācija

API bieži atgriež datus formātā, kas neatbilst jūsu lietojumprogrammas konvencijām (piemēram, snake_case pret camelCase). Proxy var pārredzami apstrādāt šo konvertēšanu.


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

// Imagine this is our raw data from an API
const apiResponse = {
  user_id: 123,
  first_name: 'Alice',
  last_name: 'Wonderland',
  account_status: 'active'
};

const camelCaseHandler = {
  get(target, property) {
    const camelCaseProperty = snakeToCamel(property);
    // Check if the camelCase version exists directly
    if (camelCaseProperty in target) {
      return target[camelCaseProperty];
    }
    // Fallback to original property name
    if (property in target) {
      return target[property];
    }
    return undefined;
  }
};

const userModel = new Proxy(apiResponse, camelCaseHandler);

// We can now access properties using camelCase, even though they are stored as snake_case
console.log(userModel.userId);        // Output: 123
console.log(userModel.firstName);     // Output: Alice
console.log(userModel.accountStatus); // Output: active

2. Novērojamie un datu piesaistīšana (mūsdienu ietvaru pamatā)

Proxy ir reaktivitātes sistēmu dzinējs tādos mūsdienu ietvaros kā Vue 3. Kad maināt rekvizītu proxy stāvokļa objektā, set slazdu var izmantot, lai aktivizētu atjauninājumus lietotāja saskarnē vai citās lietojumprogrammas daļās.

Šeit ir ļoti vienkāršots piemērs:


function createObservable(target, callback) {
  const handler = {
    set(obj, prop, value) {
      const result = Reflect.set(obj, prop, value);
      callback(prop, value); // Trigger the callback on change
      return result;
    }
  };
  return new Proxy(target, handler);
}

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

function render(prop, value) {
  console.log(`CHANGE DETECTED: The property '${prop}' was set to '${value}'. Re-rendering UI...`);
}

const observableState = createObservable(state, render);

observableState.count = 1;
// Console output: CHANGE DETECTED: The property 'count' was set to '1'. Re-rendering UI...

observableState.message = 'Goodbye';
// Console output: CHANGE DETECTED: The property 'message' was set to 'Goodbye'. Re-rendering UI...

3. Negatīvi masīva indeksi

Klasisks un jautrs piemērs ir vietējo masīvu uzvedības paplašināšana, lai atbalstītu negatīvus indeksus, kur -1 attiecas uz pēdējo elementu, līdzīgi kā tādās valodās kā Python.


function createNegativeArrayProxy(arr) {
  const handler = {
    get(target, property) {
      const index = Number(property);
      if (!Number.isNaN(index) && index < 0) {
        // Convert negative index to a positive one from the end
        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]);  // Output: a
console.log(proxiedArray[-1]); // Output: e
console.log(proxiedArray[-2]); // Output: d
console.log(proxiedArray.length); // Output: 5

Veiktspējas apsvērumi un labākā prakse

Lai gan proxy ir neticami spēcīgi, tie nav burvju lode. Ir ļoti svarīgi saprast to ietekmi.

Veiktspējas papildizdevumi

Proxy ievieš novirzīšanas slāni. Katrai darbībai ar proxy objektu jāiziet cauri apstrādātājam, kas pievieno nelielu papildizdevumu salīdzinājumā ar tiešu darbību ar vienkāršu objektu. Lielākajai daļai lietojumprogrammu (piemēram, datu validācijai vai ietvara līmeņa reaktivitātei) šis papildizdevums ir nenozīmīgs. Tomēr veiktspējai kritiskā kodā, piemēram, saspringtā ciklā, kas apstrādā miljoniem vienumu, tas var kļūt par vājā vietu. Vienmēr veiciet etalonu, ja veiktspēja ir galvenā problēma.

Proxy invarianti

Slazds nevar pilnībā melot par mērķa objekta būtību. JavaScript izpilda noteikumu kopumu, ko sauc par "invariantiem", kuriem proxy slazdiem ir jāpakļaujas. Invariantu pārkāpšana izraisīs TypeError.

Piemēram, deleteProperty slazda invariants ir tāds, ka tas nevar atgriezt true (norādot uz panākumiem), ja atbilstošais mērķa objekta rekvizīts nav konfigurējams. Tas neļauj proxy apgalvot, ka tas ir izdzēsis rekvizītu, kuru nevar izdzēst.


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

const handler = {
  deleteProperty(target, prop) {
    // This will violate the invariant
    return true;
  }
};

const proxy = new Proxy(target, handler);

try {
  delete proxy.unbreakable; // This will throw an error
} catch (e) {
  console.error(e.message);
  // Output: 'deleteProperty' on proxy: returned true for non-configurable property 'unbreakable'
}

Kad izmantot proxy (un kad nē)

Atsaucami proxy

Scenārijiem, kad jums var būt nepieciešams "izslēgt" proxy (piemēram, drošības apsvērumu vai atmiņas pārvaldības dēļ), JavaScript nodrošina Proxy.revocable(). Tas atgriež objektu, kas satur gan proxy, gan revoke funkciju.


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

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

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

// Now, we revoke the proxy's access
revoke();

try {
  console.log(proxy.data); // This will throw an error
} catch (e) {
  console.error(e.message);
  // Output: Cannot perform 'get' on a proxy that has been revoked
}

Proxy pret citiem metaprogrammēšanas paņēmieniem

Pirms Proxy izstrādātāji izmantoja citas metodes, lai sasniegtu līdzīgus mērķus. Ir noderīgi saprast, kā Proxy salīdzinās.

Object.defineProperty()

Object.defineProperty() modificē objektu tieši, definējot getterus un setterus noteiktiem rekvizītiem. Savukārt proxy nemodificē oriģinālo objektu vispār; tie to ietin.

Secinājums: Virtualizācijas spēks

JavaScript Proxy API ir vairāk nekā tikai gudra funkcija; tas ir fundamentāls pavērsiens veidā, kā mēs varam projektēt un mijiedarboties ar objektiem. Ļaujot mums pārtvert un pielāgot fundamentālās darbības, Proxy paver durvis spēcīgu modeļu pasaulei: no vienmērīgas datu validācijas un transformācijas līdz reaktīvām sistēmām, kas nodrošina mūsdienīgas lietotāja saskarnes.

Lai gan tiem ir nelielas veiktspējas izmaksas un jāievēro noteikumu kopums, to spēja izveidot tīras, atdalītas un spēcīgas abstrakcijas ir nepārspējama. Virtualizējot objektus, jūs varat izveidot sistēmas, kas ir robustākas, vieglāk uzturamas un izteiksmīgākas. Nākamreiz, kad saskaraties ar sarežģītu izaicinājumu, kas saistīts ar datu pārvaldību, validāciju vai novērojamību, apsveriet, vai Proxy ir pareizais rīks šim darbam. Tas varētu būt elegantākais risinājums jūsu rīkkopā.