Polski

Kompleksowy przewodnik dla globalnych programistów na temat opanowania JavaScript Proxy API. Naucz się przechwytywać i dostosowywać operacje na obiektach za pomocą praktycznych przykładów, przypadków użycia i wskazówek dotyczących wydajności.

JavaScript Proxy API: Dogłębne Zanurzenie w Modyfikację Zachowania Obiektów

W ewoluującym krajobrazie nowoczesnego JavaScript, programiści nieustannie poszukują potężniejszych i bardziej eleganckich sposobów zarządzania danymi i interakcji z nimi. Podczas gdy funkcje takie jak klasy, moduły i async/await zrewolucjonizowały sposób, w jaki piszemy kod, istnieje potężna funkcja metaprogramowania wprowadzona w ECMAScript 2015 (ES6), która często pozostaje niedoceniana: Proxy API.

Metaprogramowanie może brzmieć onieśmielająco, ale jest to po prostu koncepcja pisania kodu, który operuje na innym kodzie. Proxy API jest podstawowym narzędziem JavaScript do tego celu, umożliwiając tworzenie „proxy” dla innego obiektu, które może przechwytywać i redefiniować fundamentalne operacje dla tego obiektu. To tak, jakby umieścić konfigurowalnego strażnika przed obiektem, dając ci pełną kontrolę nad sposobem uzyskiwania do niego dostępu i jego modyfikacji.

Ten kompleksowy przewodnik odczaruje Proxy API. Zbadamy jego podstawowe koncepcje, rozłożymy jego różne możliwości na czynniki pierwsze za pomocą praktycznych przykładów i omówimy zaawansowane przypadki użycia i kwestie związane z wydajnością. Na koniec zrozumiesz, dlaczego Proxies są kamieniem węgielnym nowoczesnych frameworków i jak możesz wykorzystać je do pisania czystszego, potężniejszego i łatwiejszego w utrzymaniu kodu.

Zrozumienie Podstawowych Koncepcji: Target, Handler i Pułapki

Proxy API jest zbudowane na trzech podstawowych komponentach. Zrozumienie ich ról jest kluczem do opanowania proxy.

Składnia tworzenia proxy jest prosta:

const proxy = new Proxy(target, handler);

Spójrzmy na bardzo podstawowy przykład. Utworzymy proxy, które po prostu przekazuje wszystkie operacje do obiektu docelowego, używając pustego handlera.


// 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!

W tym przykładzie proxy zachowuje się dokładnie tak, jak oryginalny obiekt. Prawdziwa moc pojawia się, gdy zaczynamy definiować pułapki w handlerze.

Anatomia Proxy: Eksploracja Powszechnych Pułapek

Obiekt handler może zawierać do 13 różnych pułapek, z których każda odpowiada fundamentalnej wewnętrznej metodzie obiektów JavaScript. Zbadajmy najczęstsze i najbardziej przydatne.

Pułapki Dostępu do Właściwości

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

Jest to prawdopodobnie najczęściej używana pułapka. Jest wywoływana, gdy odczytywana jest właściwość proxy.

Przykład: Wartości domyślne dla nieistniejących właściwości.


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)`

Pułapka set jest wywoływana, gdy właściwości proxy przypisywana jest wartość. Idealnie nadaje się do walidacji, rejestrowania lub tworzenia obiektów tylko do odczytu.

Przykład: Walidacja danych.


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)`

Ta pułapka przechwytuje operator in. Umożliwia kontrolowanie, które właściwości wydają się istnieć w obiekcie.

Przykład: Ukrywanie „prywatnych” właściwości.

W JavaScript powszechną konwencją jest poprzedzanie prywatnych właściwości podkreśleniem (_). Możemy użyć pułapki has, aby ukryć je przed operatorem in.


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

Uwaga: Dotyczy to tylko operatora in. Bezpośredni dostęp, taki jak dataProxy._apiKey, nadal działałby, chyba że zaimplementujesz również odpowiednią pułapkę get.

4. `deleteProperty(target, property)`

Ta pułapka jest wykonywana, gdy właściwość jest usuwana za pomocą operatora delete. Jest przydatna do zapobiegania usuwaniu ważnych właściwości.

Pułapka musi zwrócić true dla pomyślnego usunięcia lub false dla nieudanego.

Przykład: Zapobieganie usuwaniu właściwości.


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)

Pułapki Enumeracji Obiektów i Opisu

5. `ownKeys(target)`

Ta pułapka jest wywoływana przez operacje, które pobierają listę własnych właściwości obiektu, takie jak Object.keys(), Object.getOwnPropertyNames(), Object.getOwnPropertySymbols() i Reflect.ownKeys().

Przykład: Filtrowanie kluczy.

Połączmy to z naszym poprzednim przykładem „prywatnej” właściwości, aby w pełni je ukryć.


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

Zauważ, że używamy tutaj Reflect. Obiekt Reflect udostępnia metody dla przechwytywalnych operacji JavaScript, a jego metody mają takie same nazwy i sygnatury jak pułapki proxy. Najlepszą praktyką jest używanie Reflect do przekazywania oryginalnej operacji do targetu, zapewniając poprawne zachowanie domyślne.

Pułapki Funkcji i Konstruktorów

Proxies nie ograniczają się do zwykłych obiektów. Gdy target jest funkcją, możesz przechwytywać wywołania i konstrukcje.

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

Ta pułapka jest wywoływana, gdy wykonywane jest proxy funkcji. Przechwytuje wywołanie funkcji.

Przykład: Rejestrowanie wywołań funkcji i ich argumentów.


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)`

Ta pułapka przechwytuje użycie operatora new na proxy klasy lub funkcji.

Przykład: Implementacja wzorca Singleton.


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

Praktyczne Przypadki Użycia i Zaawansowane Wzorce

Teraz, gdy omówiliśmy poszczególne pułapki, zobaczmy, jak można je połączyć, aby rozwiązywać rzeczywiste problemy.

1. Abstrakcja API i Transformacja Danych

Interfejsy API często zwracają dane w formacie, który nie pasuje do konwencji twojej aplikacji (np. snake_case vs. camelCase). Proxy może w sposób przejrzysty obsługiwać tę konwersję.


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. Obserwowalne i Powiązanie Danych (Podstawa Nowoczesnych Frameworków)

Proxies są silnikiem systemów reaktywności w nowoczesnych frameworkach, takich jak Vue 3. Kiedy zmieniasz właściwość w obiekcie stanu proxy, pułapka set może być użyta do wyzwalania aktualizacji w interfejsie użytkownika lub innych częściach aplikacji.

Oto bardzo uproszczony przykład:


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. Ujemne Indeksy Tablic

Klasycznym i zabawnym przykładem jest rozszerzenie natywnego zachowania tablic, aby obsługiwały indeksy ujemne, gdzie -1 odnosi się do ostatniego elementu, podobnie jak w językach takich jak 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

Rozważania na Temat Wydajności i Najlepsze Praktyki

Chociaż proxies są niezwykle potężne, nie są magicznym rozwiązaniem. Kluczowe jest zrozumienie ich implikacji.

Narzucona Wydajność

Proxy wprowadza warstwę pośredniczącą. Każda operacja na obiekcie proxy musi przejść przez handler, co dodaje niewielką ilość narzutu w porównaniu z bezpośrednią operacją na zwykłym obiekcie. W przypadku większości aplikacji (takich jak walidacja danych lub reaktywność na poziomie frameworka) ten narzut jest pomijalny. Jednak w kodzie o krytycznym znaczeniu dla wydajności, takim jak ciasna pętla przetwarzająca miliony elementów, może to stać się wąskim gardłem. Zawsze przeprowadzaj testy porównawcze, jeśli wydajność jest głównym problemem.

Niezmienniki Proxy

Pułapka nie może całkowicie kłamać na temat natury obiektu docelowego. JavaScript wymusza zbiór reguł zwanych „niezmiennikami”, których pułapki proxy muszą przestrzegać. Naruszenie niezmiennika spowoduje błąd TypeError.

Na przykład niezmiennikiem dla pułapki deleteProperty jest to, że nie może ona zwrócić true (wskazując na sukces), jeśli odpowiednia właściwość obiektu docelowego nie jest konfigurowalna. Zapobiega to sytuacji, w której proxy twierdzi, że usunęło właściwość, której nie można usunąć.


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'
}

Kiedy Używać Proxies (i Kiedy Nie)

Odwoływalne Proxies

W scenariuszach, w których może być konieczne „wyłączenie” proxy (np. ze względów bezpieczeństwa lub zarządzania pamięcią), JavaScript udostępnia Proxy.revocable(). Zwraca obiekt zawierający zarówno proxy, jak i funkcję revoke.


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
}

Proxies vs. Inne Techniki Metaprogramowania

Przed Proxies programiści używali innych metod do osiągnięcia podobnych celów. Warto zrozumieć, jak wypadają Proxies w porównaniu.

Object.defineProperty()

Object.defineProperty() modyfikuje obiekt bezpośrednio, definiując gettery i settery dla określonych właściwości. Z drugiej strony Proxies w ogóle nie modyfikują oryginalnego obiektu; one go owijają.

Podsumowanie: Potęga Wirtualizacji

JavaScript Proxy API to więcej niż tylko sprytna funkcja; to fundamentalna zmiana w sposobie, w jaki możemy projektować i wchodzić w interakcje z obiektami. Umożliwiając nam przechwytywanie i dostosowywanie fundamentalnych operacji, Proxies otwierają drzwi do świata potężnych wzorców: od bezproblemowej walidacji i transformacji danych po systemy reaktywne, które napędzają nowoczesne interfejsy użytkownika.

Chociaż wiążą się z niewielkim kosztem wydajności i zestawem zasad, których należy przestrzegać, ich zdolność do tworzenia czystych, luźno powiązanych i potężnych abstrakcji jest niezrównana. Wirtualizując obiekty, możesz budować systemy, które są bardziej solidne, łatwiejsze w utrzymaniu i bardziej ekspresywne. Następnym razem, gdy staniesz w obliczu złożonego wyzwania związanego z zarządzaniem danymi, walidacją lub obserwowalnością, zastanów się, czy Proxy jest odpowiednim narzędziem do tego zadania. To może być najbardziej eleganckie rozwiązanie w twoim zestawie narzędzi.