Odkryj wzorce JavaScript Proxy do modyfikacji zachowania obiektów. Dowiedz się o walidacji, wirtualizacji, śledzeniu i innych zaawansowanych technikach z przykładami kodu.
Wzorce JavaScript Proxy: Opanowanie Modyfikacji Zachowania Obiektów
Obiekt Proxy w JavaScript dostarcza potężny mechanizm do przechwytywania i dostosowywania fundamentalnych operacji na obiektach. Ta zdolność otwiera drzwi do szerokiej gamy wzorców projektowych i zaawansowanych technik kontroli zachowania obiektów. Ten kompleksowy przewodnik omawia różne wzorce Proxy, ilustrując ich zastosowania praktycznymi przykładami kodu.
Czym jest JavaScript Proxy?
Obiekt Proxy opakowuje inny obiekt (cel) i przechwytuje jego operacje. Te operacje, znane jako pułapki (traps), obejmują odczyt właściwości, przypisanie, enumerację i wywołanie funkcji. Proxy pozwala na zdefiniowanie własnej logiki, która ma być wykonana przed, po, lub zamiast tych operacji. Podstawowa koncepcja Proxy wiąże się z „metaprogramowaniem”, które umożliwia manipulowanie zachowaniem samego języka JavaScript.
Podstawowa składnia tworzenia Proxy to:
const proxy = new Proxy(target, handler);
- target: Oryginalny obiekt, który chcesz sproksować.
- handler: Obiekt zawierający metody (pułapki), które definiują, w jaki sposób Proxy przechwytuje operacje na celu.
Popularne pułapki Proxy
Obiekt handlera może definiować kilka pułapek. Oto niektóre z najczęściej używanych:
- get(target, property, receiver): Przechwytuje dostęp do właściwości (np.
obj.property
). - set(target, property, value, receiver): Przechwytuje przypisanie wartości do właściwości (np.
obj.property = value
). - has(target, property): Przechwytuje operator
in
(np.'property' in obj
). - deleteProperty(target, property): Przechwytuje operator
delete
(np.delete obj.property
). - apply(target, thisArg, argumentsList): Przechwytuje wywołania funkcji (gdy celem jest funkcja).
- construct(target, argumentsList, newTarget): Przechwytuje operator
new
(gdy celem jest funkcja konstruktora). - getPrototypeOf(target): Przechwytuje wywołania
Object.getPrototypeOf()
. - setPrototypeOf(target, prototype): Przechwytuje wywołania
Object.setPrototypeOf()
. - isExtensible(target): Przechwytuje wywołania
Object.isExtensible()
. - preventExtensions(target): Przechwytuje wywołania
Object.preventExtensions()
. - getOwnPropertyDescriptor(target, property): Przechwytuje wywołania
Object.getOwnPropertyDescriptor()
. - defineProperty(target, property, descriptor): Przechwytuje wywołania
Object.defineProperty()
. - ownKeys(target): Przechwytuje wywołania
Object.getOwnPropertyNames()
iObject.getOwnPropertySymbols()
.
Wzorce Proxy i przypadki użycia
Przyjrzyjmy się kilku popularnym wzorcom Proxy i sposobom ich zastosowania w rzeczywistych scenariuszach:
1. Walidacja
Wzorzec Walidacji używa Proxy do narzucania ograniczeń na przypisania właściwości. Jest to przydatne do zapewnienia integralności danych.
const validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('Wiek nie jest liczbą całkowitą');
}
if (value < 0) {
throw new RangeError('Wiek musi być nieujemną liczbą całkowitą');
}
}
// Domyślne zachowanie, aby zapisać wartość
obj[prop] = value;
// Sygnalizuj sukces
return true;
}
};
let person = {};
let proxy = new Proxy(person, validator);
proxy.age = 25; // Prawidłowe
console.log(proxy.age); // Wyjście: 25
try {
proxy.age = 'młody'; // Rzuca TypeError
} catch (e) {
console.log(e); // Wyjście: TypeError: Wiek nie jest liczbą całkowitą
}
try {
proxy.age = -10; // Rzuca RangeError
} catch (e) {
console.log(e); // Wyjście: RangeError: Wiek musi być nieujemną liczbą całkowitą
}
Przykład: Rozważmy platformę e-commerce, na której dane użytkownika wymagają walidacji. Proxy może wymuszać reguły dotyczące wieku, formatu e-maila, siły hasła i innych pól, zapobiegając zapisywaniu nieprawidłowych danych.
2. Wirtualizacja (Leniwe ładowanie)
Wirtualizacja, znana również jako leniwe ładowanie, opóźnia ładowanie kosztownych zasobów, dopóki nie są one faktycznie potrzebne. Proxy może działać jako zastępca (placeholder) dla rzeczywistego obiektu, ładując go dopiero po uzyskaniu dostępu do właściwości.
const expensiveData = {
load: function() {
console.log('Ładowanie kosztownych danych...');
// Symulacja czasochłonnej operacji (np. pobieranie z bazy danych)
return new Promise(resolve => {
setTimeout(() => {
resolve({
data: 'To są kosztowne dane'
});
}, 2000);
});
}
};
const lazyLoadHandler = {
get: function(target, prop) {
if (prop === 'data') {
console.log('Dostęp do danych, ładowanie w razie potrzeby...');
return target.load().then(result => {
target.data = result.data; // Zapisz załadowane dane
return result.data;
});
} else {
return target[prop];
}
}
};
const lazyData = new Proxy(expensiveData, lazyLoadHandler);
console.log('Pierwszy dostęp...');
lazyData.data.then(data => {
console.log('Dane:', data); // Wyjście: Dane: To są kosztowne dane
});
console.log('Kolejny dostęp...');
lazyData.data.then(data => {
console.log('Dane:', data); // Wyjście: Dane: To są kosztowne dane (załadowane z pamięci podręcznej)
});
Przykład: Wyobraź sobie dużą platformę mediów społecznościowych z profilami użytkowników zawierającymi liczne szczegóły i powiązane media. Natychmiastowe ładowanie wszystkich danych profilu może być nieefektywne. Wirtualizacja za pomocą Proxy pozwala najpierw załadować podstawowe informacje o profilu, a następnie dodatkowe szczegóły lub treści multimedialne dopiero wtedy, gdy użytkownik przejdzie do tych sekcji.
3. Logowanie i śledzenie
Proxy mogą być używane do śledzenia dostępu do właściwości i ich modyfikacji. Jest to cenne przy debugowaniu, audytowaniu i monitorowaniu wydajności.
const logHandler = {
get: function(target, prop, receiver) {
console.log(`GET ${prop}`);
return Reflect.get(target, prop, receiver);
},
set: function(target, prop, value) {
console.log(`SET ${prop} na ${value}`);
target[prop] = value;
return true;
}
};
let obj = { name: 'Alice' };
let proxy = new Proxy(obj, logHandler);
console.log(proxy.name); // Wyjście: GET name, Alice
proxy.age = 30; // Wyjście: SET age na 30
Przykład: W aplikacji do wspólnej edycji dokumentów, Proxy może śledzić każdą zmianę wprowadzoną w treści dokumentu. Pozwala to na tworzenie ścieżki audytu, umożliwia funkcjonalność cofania/ponawiania operacji i dostarcza wglądu we wkład poszczególnych użytkowników.
4. Widoki tylko do odczytu
Proxy mogą tworzyć widoki obiektów tylko do odczytu, zapobiegając przypadkowym modyfikacjom. Jest to przydatne do ochrony wrażliwych danych.
const readOnlyHandler = {
set: function(target, prop, value) {
console.error(`Nie można ustawić właściwości ${prop}: obiekt jest tylko do odczytu`);
return false; // Sygnalizuj, że operacja set nie powiodła się
},
deleteProperty: function(target, prop) {
console.error(`Nie można usunąć właściwości ${prop}: obiekt jest tylko do odczytu`);
return false; // Sygnalizuj, że operacja delete nie powiodła się
}
};
let data = { name: 'Bob', age: 40 };
let readOnlyData = new Proxy(data, readOnlyHandler);
try {
readOnlyData.age = 41; // Rzuca błąd
} catch (e) {
console.log(e); // Błąd nie jest rzucany, ponieważ pułapka 'set' zwraca false.
}
try {
delete readOnlyData.name; // Rzuca błąd
} catch (e) {
console.log(e); // Błąd nie jest rzucany, ponieważ pułapka 'deleteProperty' zwraca false.
}
console.log(data.age); // Wyjście: 40 (bez zmian)
Przykład: Rozważmy system finansowy, w którym niektórzy użytkownicy mają dostęp tylko do odczytu informacji o koncie. Proxy może być użyte do uniemożliwienia tym użytkownikom modyfikacji sald konta lub innych krytycznych danych.
5. Wartości domyślne
Proxy może dostarczać wartości domyślne dla brakujących właściwości. Upraszcza to kod i pozwala uniknąć sprawdzania null/undefined.
const defaultValuesHandler = {
get: function(target, prop, receiver) {
if (!(prop in target)) {
console.log(`Nie znaleziono właściwości ${prop}, zwracanie wartości domyślnej.`);
return 'Wartość Domyślna'; // Lub inną odpowiednią wartość domyślną
}
return Reflect.get(target, prop, receiver);
}
};
let config = { apiUrl: 'https://api.example.com' };
let configWithDefaults = new Proxy(config, defaultValuesHandler);
console.log(configWithDefaults.apiUrl); // Wyjście: https://api.example.com
console.log(configWithDefaults.timeout); // Wyjście: Nie znaleziono właściwości timeout, zwracanie wartości domyślnej. Wartość Domyślna
Przykład: W systemie zarządzania konfiguracją, Proxy może dostarczać domyślne wartości dla brakujących ustawień. Na przykład, jeśli plik konfiguracyjny nie określa limitu czasu połączenia z bazą danych, Proxy może zwrócić predefiniowaną wartość domyślną.
6. Metadane i adnotacje
Proxy mogą dołączać metadane lub adnotacje do obiektów, dostarczając dodatkowych informacji bez modyfikowania oryginalnego obiektu.
const metadataHandler = {
get: function(target, prop, receiver) {
if (prop === '__metadata__') {
return { description: 'To są metadane dla obiektu' };
}
return Reflect.get(target, prop, receiver);
}
};
let article = { title: 'Wprowadzenie do Proxies', content: '...' };
let articleWithMetadata = new Proxy(article, metadataHandler);
console.log(articleWithMetadata.title); // Wyjście: Wprowadzenie do Proxies
console.log(articleWithMetadata.__metadata__.description); // Wyjście: To są metadane dla obiektu
Przykład: W systemie zarządzania treścią, Proxy może dołączać metadane do artykułów, takie jak informacje o autorze, data publikacji i słowa kluczowe. Te metadane mogą być używane do wyszukiwania, filtrowania i kategoryzowania treści.
7. Przechwytywanie funkcji
Proxy mogą przechwytywać wywołania funkcji, co pozwala na dodanie logowania, walidacji lub innej logiki przetwarzania wstępnego lub końcowego.
const functionInterceptor = {
apply: function(target, thisArg, argumentsList) {
console.log('Wywoływanie funkcji z argumentami:', argumentsList);
const result = target.apply(thisArg, argumentsList);
console.log('Funkcja zwróciła:', result);
return result;
}
};
function add(a, b) {
return a + b;
}
let proxiedAdd = new Proxy(add, functionInterceptor);
let sum = proxiedAdd(5, 3); // Wyjście: Wywoływanie funkcji z argumentami: [5, 3], Funkcja zwróciła: 8
console.log(sum); // Wyjście: 8
Przykład: W aplikacji bankowej, Proxy może przechwytywać wywołania funkcji transakcyjnych, logując każdą transakcję i przeprowadzając kontrole wykrywania oszustw przed wykonaniem transakcji.
8. Przechwytywanie konstruktora
Proxy mogą przechwytywać wywołania konstruktora, co pozwala na dostosowanie tworzenia obiektów.
const constructorInterceptor = {
construct: function(target, argumentsList, newTarget) {
console.log('Tworzenie nowej instancji', target.name, 'z argumentami:', argumentsList);
const obj = new target(...argumentsList);
console.log('Nowa instancja utworzona:', obj);
return obj;
}
};
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
let ProxiedPerson = new Proxy(Person, constructorInterceptor);
let person = new ProxiedPerson('Alice', 28); // Wyjście: Tworzenie nowej instancji Person z argumentami: ['Alice', 28], Nowa instancja utworzona: Person { name: 'Alice', age: 28 }
console.log(person);
Przykład: W frameworku do tworzenia gier, Proxy może przechwytywać tworzenie obiektów gry, automatycznie przypisując im unikalne identyfikatory, dodając domyślne komponenty i rejestrując je w silniku gry.
Zaawansowane zagadnienia
- Wydajność: Chociaż Proxy oferują elastyczność, mogą wprowadzać narzut wydajnościowy. Ważne jest, aby przeprowadzać testy porównawcze i profilować kod, aby upewnić się, że korzyści z używania Proxy przeważają nad kosztami wydajności, zwłaszcza w aplikacjach krytycznych pod względem wydajności.
- Kompatybilność: Proxy są stosunkowo nowym dodatkiem do JavaScript, więc starsze przeglądarki mogą ich nie obsługiwać. Użyj wykrywania funkcji lub polyfilli, aby zapewnić kompatybilność ze starszymi środowiskami.
- Odwoływalne Proxy (Revocable Proxies): Metoda
Proxy.revocable()
tworzy Proxy, które można odwołać. Odwołanie Proxy uniemożliwia przechwytywanie jakichkolwiek dalszych operacji. Może to być przydatne ze względów bezpieczeństwa lub zarządzania zasobami. - Reflect API: API Reflect dostarcza metod do wykonywania domyślnego zachowania pułapek Proxy. Używanie
Reflect
zapewnia, że kod Proxy zachowuje się spójnie ze specyfikacją języka.
Podsumowanie
JavaScript Proxy dostarczają potężny i wszechstronny mechanizm do dostosowywania zachowania obiektów. Opanowując różne wzorce Proxy, możesz pisać bardziej solidny, łatwiejszy w utrzymaniu i wydajny kod. Niezależnie od tego, czy implementujesz walidację, wirtualizację, śledzenie, czy inne zaawansowane techniki, Proxy oferują elastyczne rozwiązanie do kontrolowania sposobu dostępu i manipulacji obiektami. Zawsze bierz pod uwagę implikacje wydajnościowe i zapewnij kompatybilność z docelowymi środowiskami. Proxy są kluczowym narzędziem w arsenale nowoczesnego dewelopera JavaScript, umożliwiając potężne techniki metaprogramowania.
Dalsza lektura
- Mozilla Developer Network (MDN): JavaScript Proxy
- Odkrywanie JavaScript Proxies: Artykuł w Smashing Magazine